@burdenoff/microfe-assethandler 2026.801.1 → 2026.810.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assethandler/pages/AssetsAssetCategoriesPage.d.ts.map +1 -1
- package/dist/assethandler/pages/AssetsAssetCategoriesPage.js +148 -147
- package/dist/assethandler/pages/AssetsAssetCategoriesPage.js.map +1 -1
- package/dist/assethandler/pages/AssetsAssetCreatePage.d.ts.map +1 -1
- package/dist/assethandler/pages/AssetsAssetCreatePage.js +229 -224
- package/dist/assethandler/pages/AssetsAssetCreatePage.js.map +1 -1
- package/dist/assethandler/pages/AssetsAssetDetailPage.d.ts.map +1 -1
- package/dist/assethandler/pages/AssetsAssetDetailPage.js +295 -293
- package/dist/assethandler/pages/AssetsAssetDetailPage.js.map +1 -1
- package/dist/assethandler/pages/AssetsAssetListPage.d.ts.map +1 -1
- package/dist/assethandler/pages/AssetsAssetListPage.js +97 -95
- package/dist/assethandler/pages/AssetsAssetListPage.js.map +1 -1
- package/dist/assethandler/providers/AssetHandlerProvider.d.ts +5 -0
- package/dist/assethandler/providers/AssetHandlerProvider.d.ts.map +1 -1
- package/dist/assethandler/providers/AssetHandlerProvider.js +9 -2
- package/dist/assethandler/providers/AssetHandlerProvider.js.map +1 -1
- package/dist/assethandler/types/index.d.ts +8 -0
- package/dist/assethandler/types/index.d.ts.map +1 -1
- package/dist/assethandler/utils/categoryPresentation.d.ts +2 -0
- package/dist/assethandler/utils/categoryPresentation.d.ts.map +1 -0
- package/dist/assethandler/utils/categoryPresentation.js +8 -0
- package/dist/assethandler/utils/categoryPresentation.js.map +1 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetsAssetCategoriesPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/AssetsAssetCategoriesPage.tsx"],"names":[],"mappings":"AAyGA,wBAAgB,yBAAyB,
|
|
1
|
+
{"version":3,"file":"AssetsAssetCategoriesPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/AssetsAssetCategoriesPage.tsx"],"names":[],"mappings":"AAyGA,wBAAgB,yBAAyB,gCAuVxC"}
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import { useAssetHandlerStore as e } from "../store/assethandlerStore.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import { formatAssetCategory as t } from "../utils/categoryPresentation.js";
|
|
3
|
+
import { useAssetCategoryPresentation as n, useAssetTerminology as r, useHiddenCategoryIds as i } from "../providers/AssetHandlerProvider.js";
|
|
4
|
+
import { useAssetHandlerNavigate as a } from "../hooks/useAssetHandlerNavigate.js";
|
|
5
|
+
import { notify as o } from "../utils/toast.js";
|
|
6
|
+
import { formatCompact as s, formatCurrencyCompact as c } from "../components/charts/chartTheme.js";
|
|
7
|
+
import { BarsChart as l } from "../components/charts/BarsChart.js";
|
|
8
|
+
import { StatTile as u } from "../components/StatTile.js";
|
|
9
|
+
import { ChartCard as d } from "../components/ChartCard.js";
|
|
9
10
|
import "../components/index.js";
|
|
10
|
-
import { useMemo as
|
|
11
|
-
import { ArrowUpDown as
|
|
12
|
-
import { Fragment as
|
|
13
|
-
import { Badge as
|
|
11
|
+
import { useMemo as f, useState as p } from "react";
|
|
12
|
+
import { ArrowUpDown as m, Box as h, Code as g, Copy as _, ExternalLink as v, Layers as y, Monitor as b, MoreHorizontal as x, Server as S, Truck as C, Wifi as w, Wrench as T } from "lucide-react";
|
|
13
|
+
import { Fragment as E, jsx as D, jsxs as O } from "react/jsx-runtime";
|
|
14
|
+
import { Badge as k, Button as A, DropdownMenu as j, DropdownMenuContent as M, DropdownMenuItem as N, DropdownMenuTrigger as P, GlassCard as F, GradientText as I, IllustratedEmptyState as L, PagePurpose as R } from "@burdenoff/fe-libs/ui";
|
|
14
15
|
//#region src/assethandler/pages/AssetsAssetCategoriesPage.tsx
|
|
15
|
-
var
|
|
16
|
-
electronics: /* @__PURE__ */
|
|
17
|
-
it_hardware: /* @__PURE__ */
|
|
18
|
-
furniture: /* @__PURE__ */
|
|
19
|
-
vehicles: /* @__PURE__ */
|
|
20
|
-
equipment: /* @__PURE__ */ T
|
|
21
|
-
software: /* @__PURE__ */
|
|
22
|
-
networking: /* @__PURE__ */
|
|
23
|
-
servers: /* @__PURE__ */
|
|
24
|
-
},
|
|
16
|
+
var z = {
|
|
17
|
+
electronics: /* @__PURE__ */ D(b, { className: "h-5 w-5" }),
|
|
18
|
+
it_hardware: /* @__PURE__ */ D(b, { className: "h-5 w-5" }),
|
|
19
|
+
furniture: /* @__PURE__ */ D(h, { className: "h-5 w-5" }),
|
|
20
|
+
vehicles: /* @__PURE__ */ D(C, { className: "h-5 w-5" }),
|
|
21
|
+
equipment: /* @__PURE__ */ D(T, { className: "h-5 w-5" }),
|
|
22
|
+
software: /* @__PURE__ */ D(g, { className: "h-5 w-5" }),
|
|
23
|
+
networking: /* @__PURE__ */ D(w, { className: "h-5 w-5" }),
|
|
24
|
+
servers: /* @__PURE__ */ D(S, { className: "h-5 w-5" })
|
|
25
|
+
}, B = {
|
|
25
26
|
electronics: "bg-status-info-bg-subtle text-status-info-text",
|
|
26
27
|
it_hardware: "bg-status-info-bg-subtle text-status-info-text",
|
|
27
28
|
furniture: "bg-status-warning-bg-subtle text-status-warning-text",
|
|
@@ -30,20 +31,20 @@ var L = {
|
|
|
30
31
|
software: "bg-action-primary-bg/10 text-action-primary-bg",
|
|
31
32
|
networking: "bg-status-info-bg-subtle text-status-info-text",
|
|
32
33
|
servers: "bg-status-error-bg-subtle text-status-error-text"
|
|
33
|
-
},
|
|
34
|
+
}, V = [
|
|
34
35
|
"bg-status-info-bg-subtle text-status-info-text",
|
|
35
36
|
"bg-status-success-bg-subtle text-status-success-text",
|
|
36
37
|
"bg-status-warning-bg-subtle text-status-warning-text",
|
|
37
38
|
"bg-status-error-bg-subtle text-status-error-text",
|
|
38
39
|
"bg-action-primary-bg/10 text-action-primary-bg"
|
|
39
40
|
];
|
|
40
|
-
function
|
|
41
|
-
if (
|
|
41
|
+
function H(e) {
|
|
42
|
+
if (B[e]) return B[e];
|
|
42
43
|
let t = 0;
|
|
43
44
|
for (let n = 0; n < e.length; n += 1) t = t * 31 + e.charCodeAt(n) >>> 0;
|
|
44
|
-
return
|
|
45
|
+
return V[t % V.length];
|
|
45
46
|
}
|
|
46
|
-
var
|
|
47
|
+
var U = [
|
|
47
48
|
{
|
|
48
49
|
key: "active",
|
|
49
50
|
label: "Active",
|
|
@@ -69,17 +70,17 @@ var V = [
|
|
|
69
70
|
label: "Disposed",
|
|
70
71
|
varName: "var(--color-text-muted)"
|
|
71
72
|
}
|
|
72
|
-
],
|
|
73
|
+
], W = {
|
|
73
74
|
excellent: 5,
|
|
74
75
|
good: 4,
|
|
75
76
|
fair: 3,
|
|
76
77
|
poor: 2,
|
|
77
78
|
damaged: 1
|
|
78
|
-
},
|
|
79
|
-
function
|
|
80
|
-
let
|
|
79
|
+
}, G = (e) => `$${e.toLocaleString()}`;
|
|
80
|
+
function K() {
|
|
81
|
+
let g = a(), b = r(), S = i(), { labels: C } = n(), { assets: w } = e(), T = f(() => w.filter((e) => !S.has(e.category)), [w, S]), [B, V] = p("count"), [K, q] = p("count"), J = f(() => {
|
|
81
82
|
let e = {};
|
|
82
|
-
for (let t of
|
|
83
|
+
for (let t of T) {
|
|
83
84
|
e[t.category] || (e[t.category] = {
|
|
84
85
|
key: t.category,
|
|
85
86
|
count: 0,
|
|
@@ -94,170 +95,170 @@ function W() {
|
|
|
94
95
|
for (let t of Object.values(e)) {
|
|
95
96
|
let e = 0, n = 0;
|
|
96
97
|
for (let [r, i] of Object.entries(t.conditions)) {
|
|
97
|
-
let t =
|
|
98
|
+
let t = W[r];
|
|
98
99
|
t && (e += t * i, n += i);
|
|
99
100
|
}
|
|
100
101
|
t.avgCondition = n > 0 ? e / n : 0;
|
|
101
102
|
}
|
|
102
103
|
return Object.values(e).sort((e, t) => t.count - e.count);
|
|
103
|
-
}, [
|
|
104
|
-
category: e.key
|
|
104
|
+
}, [T]), Y = T.length, X = f(() => T.reduce((e, t) => e + (t.currentValue ?? t.purchaseValue ?? 0), 0), [T]), Z = f(() => J.map((e) => ({
|
|
105
|
+
category: t(e.key, C),
|
|
105
106
|
count: e.count,
|
|
106
107
|
value: e.totalValue
|
|
107
|
-
})), [
|
|
108
|
-
navigator.clipboard?.writeText(e),
|
|
108
|
+
})), [J, C]), Q = f(() => [...J].sort((e, t) => K === "count" ? t.count - e.count : t.totalValue - e.totalValue), [J, K]), $ = (e) => {
|
|
109
|
+
navigator.clipboard?.writeText(e), o.success("Category copied", `"${t(e, C)}" copied to clipboard.`);
|
|
109
110
|
};
|
|
110
|
-
return /* @__PURE__ */
|
|
111
|
+
return /* @__PURE__ */ O("div", {
|
|
111
112
|
className: "space-y-6 p-4 sm:p-6",
|
|
112
113
|
children: [
|
|
113
|
-
/* @__PURE__ */
|
|
114
|
+
/* @__PURE__ */ O("div", { children: [/* @__PURE__ */ D("h1", {
|
|
114
115
|
className: "text-2xl font-bold",
|
|
115
|
-
children: /* @__PURE__ */
|
|
116
|
-
}), /* @__PURE__ */
|
|
116
|
+
children: /* @__PURE__ */ O(I, { children: [b.asset, " Categories"] })
|
|
117
|
+
}), /* @__PURE__ */ O("p", {
|
|
117
118
|
className: "text-sm text-text-secondary",
|
|
118
119
|
children: [
|
|
119
|
-
|
|
120
|
+
J.length,
|
|
120
121
|
" categories · ",
|
|
121
|
-
|
|
122
|
+
Y,
|
|
122
123
|
" total ",
|
|
123
|
-
|
|
124
|
+
b.assets.toLowerCase()
|
|
124
125
|
]
|
|
125
126
|
})] }),
|
|
126
|
-
/* @__PURE__ */
|
|
127
|
-
|
|
127
|
+
/* @__PURE__ */ D(R, { children: "Group and compare your assets by category to see where value and volume are concentrated. Each card rolls up how many assets you hold, their total book value and a proportional status mix — so you can plan procurement and replacements by category, not one asset at a time." }),
|
|
128
|
+
J.length === 0 ? /* @__PURE__ */ D(L, {
|
|
128
129
|
illustration: "empty-data",
|
|
129
130
|
title: "No categories yet",
|
|
130
|
-
description: `Add ${
|
|
131
|
-
}) : /* @__PURE__ */ E
|
|
132
|
-
/* @__PURE__ */
|
|
131
|
+
description: `Add ${b.assets.toLowerCase()} and they will be grouped into categories here automatically.`
|
|
132
|
+
}) : /* @__PURE__ */ O(E, { children: [
|
|
133
|
+
/* @__PURE__ */ O("div", {
|
|
133
134
|
className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
134
135
|
children: [
|
|
135
|
-
/* @__PURE__ */
|
|
136
|
+
/* @__PURE__ */ D(u, {
|
|
136
137
|
label: "Categories",
|
|
137
|
-
value:
|
|
138
|
-
format:
|
|
139
|
-
icon:
|
|
138
|
+
value: J.length,
|
|
139
|
+
format: s,
|
|
140
|
+
icon: y,
|
|
140
141
|
hero: !0
|
|
141
142
|
}),
|
|
142
|
-
/* @__PURE__ */
|
|
143
|
-
label: `${
|
|
144
|
-
value:
|
|
145
|
-
format:
|
|
146
|
-
icon:
|
|
143
|
+
/* @__PURE__ */ D(u, {
|
|
144
|
+
label: `${b.assets} tracked`,
|
|
145
|
+
value: Y,
|
|
146
|
+
format: s,
|
|
147
|
+
icon: h
|
|
147
148
|
}),
|
|
148
|
-
/* @__PURE__ */
|
|
149
|
+
/* @__PURE__ */ D(u, {
|
|
149
150
|
label: "Total book value",
|
|
150
|
-
value:
|
|
151
|
-
format:
|
|
152
|
-
icon:
|
|
151
|
+
value: X,
|
|
152
|
+
format: c("USD"),
|
|
153
|
+
icon: h
|
|
153
154
|
})
|
|
154
155
|
]
|
|
155
156
|
}),
|
|
156
|
-
/* @__PURE__ */
|
|
157
|
+
/* @__PURE__ */ D("div", {
|
|
157
158
|
className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3",
|
|
158
|
-
children:
|
|
159
|
-
let
|
|
160
|
-
return /* @__PURE__ */
|
|
161
|
-
treatment:
|
|
162
|
-
glow:
|
|
159
|
+
children: J.map((e, n) => {
|
|
160
|
+
let r = Y > 0 ? Math.round(e.count / Y * 100) : 0, i = U.filter((t) => (e.statuses[t.key] || 0) > 0);
|
|
161
|
+
return /* @__PURE__ */ D(F, {
|
|
162
|
+
treatment: n === 0 ? "glass" : "plain",
|
|
163
|
+
glow: n === 0,
|
|
163
164
|
className: "border-border-seam",
|
|
164
|
-
children: /* @__PURE__ */
|
|
165
|
+
children: /* @__PURE__ */ O("div", {
|
|
165
166
|
className: "space-y-4 p-5",
|
|
166
167
|
children: [
|
|
167
|
-
/* @__PURE__ */
|
|
168
|
+
/* @__PURE__ */ O("div", {
|
|
168
169
|
className: "flex items-start justify-between gap-2",
|
|
169
|
-
children: [/* @__PURE__ */
|
|
170
|
+
children: [/* @__PURE__ */ O("div", {
|
|
170
171
|
className: "flex min-w-0 items-center gap-3",
|
|
171
|
-
children: [/* @__PURE__ */
|
|
172
|
-
className: `shrink-0 rounded-lg p-2 ${
|
|
173
|
-
children:
|
|
174
|
-
}), /* @__PURE__ */
|
|
172
|
+
children: [/* @__PURE__ */ D("div", {
|
|
173
|
+
className: `shrink-0 rounded-lg p-2 ${H(e.key)}`,
|
|
174
|
+
children: z[e.key] || /* @__PURE__ */ D(h, { className: "h-5 w-5" })
|
|
175
|
+
}), /* @__PURE__ */ O("div", {
|
|
175
176
|
className: "min-w-0",
|
|
176
|
-
children: [/* @__PURE__ */
|
|
177
|
+
children: [/* @__PURE__ */ D("p", {
|
|
177
178
|
className: "truncate text-base font-semibold capitalize",
|
|
178
|
-
children: e.key
|
|
179
|
-
}), /* @__PURE__ */
|
|
179
|
+
children: t(e.key, C)
|
|
180
|
+
}), /* @__PURE__ */ O(k, {
|
|
180
181
|
variant: "outline",
|
|
181
182
|
className: "mt-0.5 text-xs",
|
|
182
|
-
children: [
|
|
183
|
+
children: [r, "% of estate"]
|
|
183
184
|
})]
|
|
184
185
|
})]
|
|
185
|
-
}), /* @__PURE__ */
|
|
186
|
+
}), /* @__PURE__ */ O(j, { children: [/* @__PURE__ */ D(P, {
|
|
186
187
|
asChild: !0,
|
|
187
|
-
children: /* @__PURE__ */
|
|
188
|
+
children: /* @__PURE__ */ D(A, {
|
|
188
189
|
variant: "ghost",
|
|
189
190
|
size: "icon",
|
|
190
191
|
className: "h-8 w-8 shrink-0",
|
|
191
|
-
"aria-label": `Actions for ${e.key
|
|
192
|
-
children: /* @__PURE__ */
|
|
192
|
+
"aria-label": `Actions for ${t(e.key, C)}`,
|
|
193
|
+
children: /* @__PURE__ */ D(x, { className: "h-4 w-4" })
|
|
193
194
|
})
|
|
194
|
-
}), /* @__PURE__ */
|
|
195
|
+
}), /* @__PURE__ */ O(M, {
|
|
195
196
|
align: "end",
|
|
196
|
-
children: [/* @__PURE__ */
|
|
197
|
-
onClick: () =>
|
|
197
|
+
children: [/* @__PURE__ */ O(N, {
|
|
198
|
+
onClick: () => g("/assets/asset-list"),
|
|
198
199
|
children: [
|
|
199
|
-
/* @__PURE__ */
|
|
200
|
+
/* @__PURE__ */ D(v, { className: "mr-2 h-4 w-4" }),
|
|
200
201
|
"View ",
|
|
201
|
-
|
|
202
|
+
b.assets.toLowerCase()
|
|
202
203
|
]
|
|
203
|
-
}), /* @__PURE__ */
|
|
204
|
-
onClick: () =>
|
|
205
|
-
children: [/* @__PURE__ */
|
|
204
|
+
}), /* @__PURE__ */ O(N, {
|
|
205
|
+
onClick: () => $(e.key),
|
|
206
|
+
children: [/* @__PURE__ */ D(_, { className: "mr-2 h-4 w-4" }), "Copy category ID"]
|
|
206
207
|
})]
|
|
207
208
|
})] })]
|
|
208
209
|
}),
|
|
209
|
-
/* @__PURE__ */
|
|
210
|
+
/* @__PURE__ */ O("div", {
|
|
210
211
|
className: "flex flex-wrap items-end justify-between gap-3",
|
|
211
|
-
children: [/* @__PURE__ */
|
|
212
|
+
children: [/* @__PURE__ */ O("div", { children: [/* @__PURE__ */ O("p", {
|
|
212
213
|
className: "text-xs text-text-secondary",
|
|
213
|
-
children: [
|
|
214
|
-
}), /* @__PURE__ */
|
|
214
|
+
children: [b.asset, " count"]
|
|
215
|
+
}), /* @__PURE__ */ D("p", {
|
|
215
216
|
className: "text-2xl font-bold tabular-nums",
|
|
216
217
|
children: e.count
|
|
217
|
-
})] }), /* @__PURE__ */
|
|
218
|
+
})] }), /* @__PURE__ */ O("div", {
|
|
218
219
|
className: "text-right",
|
|
219
|
-
children: [/* @__PURE__ */
|
|
220
|
+
children: [/* @__PURE__ */ D("p", {
|
|
220
221
|
className: "text-xs text-text-secondary",
|
|
221
222
|
children: "Total value"
|
|
222
|
-
}), /* @__PURE__ */
|
|
223
|
+
}), /* @__PURE__ */ D("p", {
|
|
223
224
|
className: "text-2xl font-bold tabular-nums",
|
|
224
|
-
children:
|
|
225
|
+
children: c("USD")(e.totalValue)
|
|
225
226
|
})]
|
|
226
227
|
})]
|
|
227
228
|
}),
|
|
228
|
-
/* @__PURE__ */
|
|
229
|
+
/* @__PURE__ */ O("div", { children: [/* @__PURE__ */ D("div", {
|
|
229
230
|
className: "flex h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
230
231
|
role: "img",
|
|
231
|
-
"aria-label":
|
|
232
|
-
children:
|
|
232
|
+
"aria-label": i.map((t) => `${t.label}: ${e.statuses[t.key]}`).join(", "),
|
|
233
|
+
children: i.map((t) => /* @__PURE__ */ D("div", { style: {
|
|
233
234
|
width: `${(e.statuses[t.key] || 0) / e.count * 100}%`,
|
|
234
235
|
backgroundColor: t.varName
|
|
235
236
|
} }, t.key))
|
|
236
|
-
}), /* @__PURE__ */
|
|
237
|
+
}), /* @__PURE__ */ D("div", {
|
|
237
238
|
className: "mt-2 flex flex-wrap gap-x-3 gap-y-1",
|
|
238
|
-
children:
|
|
239
|
+
children: i.map((t) => /* @__PURE__ */ O("span", {
|
|
239
240
|
className: "inline-flex items-center gap-1.5 text-xs text-text-secondary",
|
|
240
241
|
children: [
|
|
241
|
-
/* @__PURE__ */
|
|
242
|
+
/* @__PURE__ */ D("span", {
|
|
242
243
|
className: "h-2.5 w-2.5 rounded-[3px]",
|
|
243
244
|
style: { backgroundColor: t.varName }
|
|
244
245
|
}),
|
|
245
246
|
t.label,
|
|
246
|
-
/* @__PURE__ */
|
|
247
|
+
/* @__PURE__ */ D("span", {
|
|
247
248
|
className: "tabular-nums text-text-muted",
|
|
248
249
|
children: e.statuses[t.key]
|
|
249
250
|
})
|
|
250
251
|
]
|
|
251
252
|
}, t.key))
|
|
252
253
|
})] }),
|
|
253
|
-
/* @__PURE__ */
|
|
254
|
+
/* @__PURE__ */ O("div", {
|
|
254
255
|
className: "flex items-center justify-between border-t border-border-seam pt-3 text-sm",
|
|
255
|
-
children: [/* @__PURE__ */
|
|
256
|
+
children: [/* @__PURE__ */ D("span", {
|
|
256
257
|
className: "text-text-secondary",
|
|
257
258
|
children: "Avg. condition"
|
|
258
|
-
}), /* @__PURE__ */
|
|
259
|
+
}), /* @__PURE__ */ O("span", {
|
|
259
260
|
className: "font-semibold tabular-nums",
|
|
260
|
-
children: [e.avgCondition > 0 ? e.avgCondition.toFixed(1) : "—", /* @__PURE__ */
|
|
261
|
+
children: [e.avgCondition > 0 ? e.avgCondition.toFixed(1) : "—", /* @__PURE__ */ D("span", {
|
|
261
262
|
className: "text-xs font-normal text-text-muted",
|
|
262
263
|
children: " / 5"
|
|
263
264
|
})]
|
|
@@ -268,92 +269,92 @@ function W() {
|
|
|
268
269
|
}, e.key);
|
|
269
270
|
})
|
|
270
271
|
}),
|
|
271
|
-
/* @__PURE__ */
|
|
272
|
+
/* @__PURE__ */ D(d, {
|
|
272
273
|
title: "Category comparison",
|
|
273
|
-
subtitle:
|
|
274
|
-
actions: /* @__PURE__ */
|
|
274
|
+
subtitle: B === "count" ? `${b.assets} per category` : "Book value per category",
|
|
275
|
+
actions: /* @__PURE__ */ O("div", {
|
|
275
276
|
className: "flex gap-1 rounded-md border border-border-seam p-0.5",
|
|
276
|
-
children: [/* @__PURE__ */
|
|
277
|
-
variant:
|
|
277
|
+
children: [/* @__PURE__ */ D(A, {
|
|
278
|
+
variant: B === "count" ? "default" : "ghost",
|
|
278
279
|
size: "sm",
|
|
279
280
|
className: "h-7 px-2 text-xs",
|
|
280
|
-
onClick: () =>
|
|
281
|
+
onClick: () => V("count"),
|
|
281
282
|
children: "Count"
|
|
282
|
-
}), /* @__PURE__ */
|
|
283
|
-
variant:
|
|
283
|
+
}), /* @__PURE__ */ D(A, {
|
|
284
|
+
variant: B === "value" ? "default" : "ghost",
|
|
284
285
|
size: "sm",
|
|
285
286
|
className: "h-7 px-2 text-xs",
|
|
286
|
-
onClick: () =>
|
|
287
|
+
onClick: () => V("value"),
|
|
287
288
|
children: "Value"
|
|
288
289
|
})]
|
|
289
290
|
}),
|
|
290
|
-
children: /* @__PURE__ */
|
|
291
|
-
data:
|
|
291
|
+
children: /* @__PURE__ */ D(l, {
|
|
292
|
+
data: Z,
|
|
292
293
|
xKey: "category",
|
|
293
294
|
orientation: "horizontal",
|
|
294
|
-
height: Math.max(200,
|
|
295
|
+
height: Math.max(200, Z.length * 44),
|
|
295
296
|
categoryWidth: 120,
|
|
296
|
-
valueFormat:
|
|
297
|
+
valueFormat: B === "value" ? c("USD") : s,
|
|
297
298
|
series: [{
|
|
298
|
-
key:
|
|
299
|
-
label:
|
|
299
|
+
key: B,
|
|
300
|
+
label: B === "count" ? b.assets : "Book value"
|
|
300
301
|
}]
|
|
301
302
|
})
|
|
302
303
|
}),
|
|
303
|
-
/* @__PURE__ */
|
|
304
|
+
/* @__PURE__ */ D(d, {
|
|
304
305
|
title: "Category summary",
|
|
305
306
|
subtitle: "Sortable roll-up across every category",
|
|
306
|
-
icon:
|
|
307
|
-
children: /* @__PURE__ */
|
|
307
|
+
icon: y,
|
|
308
|
+
children: /* @__PURE__ */ D("div", {
|
|
308
309
|
className: "overflow-x-auto",
|
|
309
|
-
children: /* @__PURE__ */
|
|
310
|
+
children: /* @__PURE__ */ O("table", {
|
|
310
311
|
className: "w-full min-w-[520px] text-sm",
|
|
311
|
-
children: [/* @__PURE__ */
|
|
312
|
+
children: [/* @__PURE__ */ D("thead", { children: /* @__PURE__ */ O("tr", {
|
|
312
313
|
className: "border-b border-border-seam text-left text-text-secondary",
|
|
313
314
|
children: [
|
|
314
|
-
/* @__PURE__ */
|
|
315
|
+
/* @__PURE__ */ D("th", {
|
|
315
316
|
className: "py-2 pr-4 font-medium",
|
|
316
317
|
children: "Category"
|
|
317
318
|
}),
|
|
318
|
-
/* @__PURE__ */
|
|
319
|
+
/* @__PURE__ */ D("th", {
|
|
319
320
|
className: "py-2 pr-4 font-medium",
|
|
320
|
-
children: /* @__PURE__ */
|
|
321
|
+
children: /* @__PURE__ */ O("button", {
|
|
321
322
|
type: "button",
|
|
322
323
|
className: "inline-flex items-center gap-1 hover:text-text-primary",
|
|
323
|
-
onClick: () =>
|
|
324
|
-
children: ["Count", /* @__PURE__ */
|
|
324
|
+
onClick: () => q("count"),
|
|
325
|
+
children: ["Count", /* @__PURE__ */ D(m, { className: `h-3 w-3 ${K === "count" ? "text-action-primary-bg" : ""}` })]
|
|
325
326
|
})
|
|
326
327
|
}),
|
|
327
|
-
/* @__PURE__ */
|
|
328
|
+
/* @__PURE__ */ D("th", {
|
|
328
329
|
className: "py-2 pr-4 font-medium",
|
|
329
|
-
children: /* @__PURE__ */
|
|
330
|
+
children: /* @__PURE__ */ O("button", {
|
|
330
331
|
type: "button",
|
|
331
332
|
className: "inline-flex items-center gap-1 hover:text-text-primary",
|
|
332
|
-
onClick: () =>
|
|
333
|
-
children: ["Value", /* @__PURE__ */
|
|
333
|
+
onClick: () => q("value"),
|
|
334
|
+
children: ["Value", /* @__PURE__ */ D(m, { className: `h-3 w-3 ${K === "value" ? "text-action-primary-bg" : ""}` })]
|
|
334
335
|
})
|
|
335
336
|
}),
|
|
336
|
-
/* @__PURE__ */
|
|
337
|
+
/* @__PURE__ */ D("th", {
|
|
337
338
|
className: "py-2 font-medium",
|
|
338
339
|
children: "Avg. condition"
|
|
339
340
|
})
|
|
340
341
|
]
|
|
341
|
-
}) }), /* @__PURE__ */
|
|
342
|
+
}) }), /* @__PURE__ */ D("tbody", { children: Q.map((e) => /* @__PURE__ */ O("tr", {
|
|
342
343
|
className: "border-b border-border-seam/60 last:border-0",
|
|
343
344
|
children: [
|
|
344
|
-
/* @__PURE__ */
|
|
345
|
+
/* @__PURE__ */ D("td", {
|
|
345
346
|
className: "py-2.5 pr-4 font-medium capitalize",
|
|
346
|
-
children: e.key
|
|
347
|
+
children: t(e.key, C)
|
|
347
348
|
}),
|
|
348
|
-
/* @__PURE__ */
|
|
349
|
+
/* @__PURE__ */ D("td", {
|
|
349
350
|
className: "py-2.5 pr-4 tabular-nums",
|
|
350
351
|
children: e.count
|
|
351
352
|
}),
|
|
352
|
-
/* @__PURE__ */
|
|
353
|
+
/* @__PURE__ */ D("td", {
|
|
353
354
|
className: "py-2.5 pr-4 tabular-nums",
|
|
354
|
-
children:
|
|
355
|
+
children: G(e.totalValue)
|
|
355
356
|
}),
|
|
356
|
-
/* @__PURE__ */
|
|
357
|
+
/* @__PURE__ */ D("td", {
|
|
357
358
|
className: "py-2.5 tabular-nums",
|
|
358
359
|
children: e.avgCondition > 0 ? e.avgCondition.toFixed(1) : "—"
|
|
359
360
|
})
|
|
@@ -367,6 +368,6 @@ function W() {
|
|
|
367
368
|
});
|
|
368
369
|
}
|
|
369
370
|
//#endregion
|
|
370
|
-
export {
|
|
371
|
+
export { K as AssetsAssetCategoriesPage };
|
|
371
372
|
|
|
372
373
|
//# sourceMappingURL=AssetsAssetCategoriesPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetsAssetCategoriesPage.js","names":[],"sources":["../../../src/assethandler/pages/AssetsAssetCategoriesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n GradientText,\n GlassCard,\n PagePurpose,\n IllustratedEmptyState,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Box,\n Monitor,\n Server,\n Wifi,\n Truck,\n Wrench,\n Code,\n MoreHorizontal,\n Copy,\n ExternalLink,\n ArrowUpDown,\n Layers,\n} from 'lucide-react';\nimport { useAssetHandlerNavigate } from '../hooks/useAssetHandlerNavigate';\nimport { useAssetTerminology, useHiddenCategoryIds } from '../providers/AssetHandlerProvider';\nimport { useAssetHandlerStore, type AssetStatus } from '../store/assethandlerStore';\nimport { notify } from '../utils/toast';\nimport {\n ChartCard,\n StatTile,\n BarsChart,\n formatCompact,\n formatCurrencyCompact,\n} from '../components';\n\nconst categoryIcons: Record<string, React.ReactNode> = {\n electronics: <Monitor className=\"h-5 w-5\" />,\n it_hardware: <Monitor className=\"h-5 w-5\" />,\n furniture: <Box className=\"h-5 w-5\" />,\n vehicles: <Truck className=\"h-5 w-5\" />,\n equipment: <Wrench className=\"h-5 w-5\" />,\n software: <Code className=\"h-5 w-5\" />,\n networking: <Wifi className=\"h-5 w-5\" />,\n servers: <Server className=\"h-5 w-5\" />,\n};\n\nconst categoryColors: Record<string, string> = {\n electronics: 'bg-status-info-bg-subtle text-status-info-text',\n it_hardware: 'bg-status-info-bg-subtle text-status-info-text',\n furniture: 'bg-status-warning-bg-subtle text-status-warning-text',\n vehicles: 'bg-status-success-bg-subtle text-status-success-text',\n equipment: 'bg-status-warning-bg-subtle text-status-warning-text',\n software: 'bg-action-primary-bg/10 text-action-primary-bg',\n networking: 'bg-status-info-bg-subtle text-status-info-text',\n servers: 'bg-status-error-bg-subtle text-status-error-text',\n};\n\n// Stable fallback tint for any category outside the known slug map — hashes the\n// key into the semantic palette so every real category is visually differentiated.\nconst FALLBACK_TINTS = [\n 'bg-status-info-bg-subtle text-status-info-text',\n 'bg-status-success-bg-subtle text-status-success-text',\n 'bg-status-warning-bg-subtle text-status-warning-text',\n 'bg-status-error-bg-subtle text-status-error-text',\n 'bg-action-primary-bg/10 text-action-primary-bg',\n];\nfunction tintFor(key: string): string {\n if (categoryColors[key]) return categoryColors[key];\n let hash = 0;\n for (let i = 0; i < key.length; i += 1) hash = (hash * 31 + key.charCodeAt(i)) >>> 0;\n return FALLBACK_TINTS[hash % FALLBACK_TINTS.length];\n}\n\n// Proportional status-bar segments — semantic tokens (CSS vars), theme-aware.\nconst STATUS_SEGMENTS: { key: AssetStatus; label: string; varName: string }[] = [\n { key: 'active', label: 'Active', varName: 'var(--color-status-success-bg)' },\n { key: 'maintenance', label: 'Maintenance', varName: 'var(--color-status-warning-bg)' },\n { key: 'retired', label: 'Retired', varName: 'var(--color-status-error-bg)' },\n { key: 'inactive', label: 'Inactive', varName: 'var(--color-status-info-bg)' },\n { key: 'disposed', label: 'Disposed', varName: 'var(--color-text-muted)' },\n];\n\nconst CONDITION_SCORE: Record<string, number> = {\n excellent: 5,\n good: 4,\n fair: 3,\n poor: 2,\n damaged: 1,\n};\n\ninterface CategoryAggregate {\n key: string;\n count: number;\n totalValue: number;\n statuses: Record<string, number>;\n conditions: Record<string, number>;\n avgCondition: number;\n}\n\nconst money = (value: number): string => `$${value.toLocaleString()}`;\n\nexport function AssetsAssetCategoriesPage() {\n const navigate = useAssetHandlerNavigate();\n const term = useAssetTerminology();\n const hiddenCategories = useHiddenCategoryIds();\n const { assets: allAssets } = useAssetHandlerStore();\n const assets = useMemo(\n () => allAssets.filter((a) => !hiddenCategories.has(a.category)),\n [allAssets, hiddenCategories]\n );\n const [chartMetric, setChartMetric] = useState<'count' | 'value'>('count');\n const [sortKey, setSortKey] = useState<'count' | 'value'>('count');\n\n const aggregates = useMemo<CategoryAggregate[]>(() => {\n const map: Record<string, CategoryAggregate> = {};\n for (const asset of assets) {\n if (!map[asset.category]) {\n map[asset.category] = {\n key: asset.category,\n count: 0,\n totalValue: 0,\n statuses: {},\n conditions: {},\n avgCondition: 0,\n };\n }\n const cat = map[asset.category];\n cat.count += 1;\n cat.totalValue += asset.currentValue ?? asset.purchaseValue ?? 0;\n cat.statuses[asset.status] = (cat.statuses[asset.status] || 0) + 1;\n cat.conditions[asset.condition] = (cat.conditions[asset.condition] || 0) + 1;\n }\n for (const cat of Object.values(map)) {\n let scoreSum = 0;\n let scoreN = 0;\n for (const [cond, n] of Object.entries(cat.conditions)) {\n const score = CONDITION_SCORE[cond];\n if (score) {\n scoreSum += score * n;\n scoreN += n;\n }\n }\n cat.avgCondition = scoreN > 0 ? scoreSum / scoreN : 0;\n }\n return Object.values(map).sort((a, b) => b.count - a.count);\n }, [assets]);\n\n const totalAssets = assets.length;\n const totalValue = useMemo(\n () => assets.reduce((sum, a) => sum + (a.currentValue ?? a.purchaseValue ?? 0), 0),\n [assets]\n );\n\n const chartData = useMemo(\n () =>\n aggregates.map((cat) => ({\n category: cat.key.replace(/_/g, ' '),\n count: cat.count,\n value: cat.totalValue,\n })),\n [aggregates]\n );\n\n const sortedRows = useMemo(\n () =>\n [...aggregates].sort((a, b) =>\n sortKey === 'count' ? b.count - a.count : b.totalValue - a.totalValue\n ),\n [aggregates, sortKey]\n );\n\n const copyId = (key: string) => {\n void navigator.clipboard?.writeText(key);\n notify.success('Category copied', `\"${key.replace(/_/g, ' ')}\" copied to clipboard.`);\n };\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\">\n <div>\n <h1 className=\"text-2xl font-bold\">\n <GradientText>{term.asset} Categories</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {aggregates.length} categories · {totalAssets} total {term.assets.toLowerCase()}\n </p>\n </div>\n\n <PagePurpose>\n Group and compare your assets by category to see where value and volume are concentrated.\n Each card rolls up how many assets you hold, their total book value and a proportional\n status mix — so you can plan procurement and replacements by category, not one asset at a\n time.\n </PagePurpose>\n\n {aggregates.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No categories yet\"\n description={`Add ${term.assets.toLowerCase()} and they will be grouped into categories here automatically.`}\n />\n ) : (\n <>\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <StatTile\n label=\"Categories\"\n value={aggregates.length}\n format={formatCompact}\n icon={Layers}\n hero\n />\n <StatTile\n label={`${term.assets} tracked`}\n value={totalAssets}\n format={formatCompact}\n icon={Box}\n />\n <StatTile\n label=\"Total book value\"\n value={totalValue}\n format={formatCurrencyCompact('USD')}\n icon={Box}\n />\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {aggregates.map((cat, idx) => {\n const pct = totalAssets > 0 ? Math.round((cat.count / totalAssets) * 100) : 0;\n const segments = STATUS_SEGMENTS.filter((s) => (cat.statuses[s.key] || 0) > 0);\n return (\n <GlassCard\n key={cat.key}\n treatment={idx === 0 ? 'glass' : 'plain'}\n glow={idx === 0}\n className=\"border-border-seam\"\n >\n <div className=\"space-y-4 p-5\">\n {/* Header: icon + title + overflow */}\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className={`shrink-0 rounded-lg p-2 ${tintFor(cat.key)}`}>\n {categoryIcons[cat.key] || <Box className=\"h-5 w-5\" />}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-base font-semibold capitalize\">\n {cat.key.replace(/_/g, ' ')}\n </p>\n <Badge variant=\"outline\" className=\"mt-0.5 text-xs\">\n {pct}% of estate\n </Badge>\n </div>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 shrink-0\"\n aria-label={`Actions for ${cat.key.replace(/_/g, ' ')}`}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => navigate('/assets/asset-list')}>\n <ExternalLink className=\"mr-2 h-4 w-4\" />\n View {term.assets.toLowerCase()}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => copyId(cat.key)}>\n <Copy className=\"mr-2 h-4 w-4\" />\n Copy category ID\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n\n {/* Dual metric row */}\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <div>\n <p className=\"text-xs text-text-secondary\">{term.asset} count</p>\n <p className=\"text-2xl font-bold tabular-nums\">{cat.count}</p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-xs text-text-secondary\">Total value</p>\n <p className=\"text-2xl font-bold tabular-nums\">\n {formatCurrencyCompact('USD')(cat.totalValue)}\n </p>\n </div>\n </div>\n\n {/* Proportional status bar */}\n <div>\n <div\n className=\"flex h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"img\"\n aria-label={segments\n .map((s) => `${s.label}: ${cat.statuses[s.key]}`)\n .join(', ')}\n >\n {segments.map((s) => (\n <div\n key={s.key}\n style={{\n width: `${((cat.statuses[s.key] || 0) / cat.count) * 100}%`,\n backgroundColor: s.varName,\n }}\n />\n ))}\n </div>\n <div className=\"mt-2 flex flex-wrap gap-x-3 gap-y-1\">\n {segments.map((s) => (\n <span\n key={s.key}\n className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary\"\n >\n <span\n className=\"h-2.5 w-2.5 rounded-[3px]\"\n style={{ backgroundColor: s.varName }}\n />\n {s.label}\n <span className=\"tabular-nums text-text-muted\">\n {cat.statuses[s.key]}\n </span>\n </span>\n ))}\n </div>\n </div>\n\n {/* Avg condition */}\n <div className=\"flex items-center justify-between border-t border-border-seam pt-3 text-sm\">\n <span className=\"text-text-secondary\">Avg. condition</span>\n <span className=\"font-semibold tabular-nums\">\n {cat.avgCondition > 0 ? cat.avgCondition.toFixed(1) : '—'}\n <span className=\"text-xs font-normal text-text-muted\"> / 5</span>\n </span>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {/* Cross-category comparison chart */}\n <ChartCard\n title=\"Category comparison\"\n subtitle={\n chartMetric === 'count' ? `${term.assets} per category` : 'Book value per category'\n }\n actions={\n <div className=\"flex gap-1 rounded-md border border-border-seam p-0.5\">\n <Button\n variant={chartMetric === 'count' ? 'default' : 'ghost'}\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={() => setChartMetric('count')}\n >\n Count\n </Button>\n <Button\n variant={chartMetric === 'value' ? 'default' : 'ghost'}\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={() => setChartMetric('value')}\n >\n Value\n </Button>\n </div>\n }\n >\n <BarsChart\n data={chartData}\n xKey=\"category\"\n orientation=\"horizontal\"\n height={Math.max(200, chartData.length * 44)}\n categoryWidth={120}\n valueFormat={chartMetric === 'value' ? formatCurrencyCompact('USD') : formatCompact}\n series={[\n {\n key: chartMetric,\n label: chartMetric === 'count' ? term.assets : 'Book value',\n },\n ]}\n />\n </ChartCard>\n\n {/* Summary comparison table */}\n <ChartCard\n title=\"Category summary\"\n subtitle=\"Sortable roll-up across every category\"\n icon={Layers}\n >\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[520px] text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-left text-text-secondary\">\n <th className=\"py-2 pr-4 font-medium\">Category</th>\n <th className=\"py-2 pr-4 font-medium\">\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:text-text-primary\"\n onClick={() => setSortKey('count')}\n >\n Count\n <ArrowUpDown\n className={`h-3 w-3 ${sortKey === 'count' ? 'text-action-primary-bg' : ''}`}\n />\n </button>\n </th>\n <th className=\"py-2 pr-4 font-medium\">\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:text-text-primary\"\n onClick={() => setSortKey('value')}\n >\n Value\n <ArrowUpDown\n className={`h-3 w-3 ${sortKey === 'value' ? 'text-action-primary-bg' : ''}`}\n />\n </button>\n </th>\n <th className=\"py-2 font-medium\">Avg. condition</th>\n </tr>\n </thead>\n <tbody>\n {sortedRows.map((cat) => (\n <tr key={cat.key} className=\"border-b border-border-seam/60 last:border-0\">\n <td className=\"py-2.5 pr-4 font-medium capitalize\">\n {cat.key.replace(/_/g, ' ')}\n </td>\n <td className=\"py-2.5 pr-4 tabular-nums\">{cat.count}</td>\n <td className=\"py-2.5 pr-4 tabular-nums\">{money(cat.totalValue)}</td>\n <td className=\"py-2.5 tabular-nums\">\n {cat.avgCondition > 0 ? cat.avgCondition.toFixed(1) : '—'}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </ChartCard>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssetsAssetCategoriesPage.js","names":[],"sources":["../../../src/assethandler/pages/AssetsAssetCategoriesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n GradientText,\n GlassCard,\n PagePurpose,\n IllustratedEmptyState,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Box,\n Monitor,\n Server,\n Wifi,\n Truck,\n Wrench,\n Code,\n MoreHorizontal,\n Copy,\n ExternalLink,\n ArrowUpDown,\n Layers,\n} from 'lucide-react';\nimport { useAssetHandlerNavigate } from '../hooks/useAssetHandlerNavigate';\nimport { formatAssetCategory, useAssetCategoryPresentation, useAssetTerminology, useHiddenCategoryIds } from '../providers/AssetHandlerProvider';\nimport { useAssetHandlerStore, type AssetStatus } from '../store/assethandlerStore';\nimport { notify } from '../utils/toast';\nimport {\n ChartCard,\n StatTile,\n BarsChart,\n formatCompact,\n formatCurrencyCompact,\n} from '../components';\n\nconst categoryIcons: Record<string, React.ReactNode> = {\n electronics: <Monitor className=\"h-5 w-5\" />,\n it_hardware: <Monitor className=\"h-5 w-5\" />,\n furniture: <Box className=\"h-5 w-5\" />,\n vehicles: <Truck className=\"h-5 w-5\" />,\n equipment: <Wrench className=\"h-5 w-5\" />,\n software: <Code className=\"h-5 w-5\" />,\n networking: <Wifi className=\"h-5 w-5\" />,\n servers: <Server className=\"h-5 w-5\" />,\n};\n\nconst categoryColors: Record<string, string> = {\n electronics: 'bg-status-info-bg-subtle text-status-info-text',\n it_hardware: 'bg-status-info-bg-subtle text-status-info-text',\n furniture: 'bg-status-warning-bg-subtle text-status-warning-text',\n vehicles: 'bg-status-success-bg-subtle text-status-success-text',\n equipment: 'bg-status-warning-bg-subtle text-status-warning-text',\n software: 'bg-action-primary-bg/10 text-action-primary-bg',\n networking: 'bg-status-info-bg-subtle text-status-info-text',\n servers: 'bg-status-error-bg-subtle text-status-error-text',\n};\n\n// Stable fallback tint for any category outside the known slug map — hashes the\n// key into the semantic palette so every real category is visually differentiated.\nconst FALLBACK_TINTS = [\n 'bg-status-info-bg-subtle text-status-info-text',\n 'bg-status-success-bg-subtle text-status-success-text',\n 'bg-status-warning-bg-subtle text-status-warning-text',\n 'bg-status-error-bg-subtle text-status-error-text',\n 'bg-action-primary-bg/10 text-action-primary-bg',\n];\nfunction tintFor(key: string): string {\n if (categoryColors[key]) return categoryColors[key];\n let hash = 0;\n for (let i = 0; i < key.length; i += 1) hash = (hash * 31 + key.charCodeAt(i)) >>> 0;\n return FALLBACK_TINTS[hash % FALLBACK_TINTS.length];\n}\n\n// Proportional status-bar segments — semantic tokens (CSS vars), theme-aware.\nconst STATUS_SEGMENTS: { key: AssetStatus; label: string; varName: string }[] = [\n { key: 'active', label: 'Active', varName: 'var(--color-status-success-bg)' },\n { key: 'maintenance', label: 'Maintenance', varName: 'var(--color-status-warning-bg)' },\n { key: 'retired', label: 'Retired', varName: 'var(--color-status-error-bg)' },\n { key: 'inactive', label: 'Inactive', varName: 'var(--color-status-info-bg)' },\n { key: 'disposed', label: 'Disposed', varName: 'var(--color-text-muted)' },\n];\n\nconst CONDITION_SCORE: Record<string, number> = {\n excellent: 5,\n good: 4,\n fair: 3,\n poor: 2,\n damaged: 1,\n};\n\ninterface CategoryAggregate {\n key: string;\n count: number;\n totalValue: number;\n statuses: Record<string, number>;\n conditions: Record<string, number>;\n avgCondition: number;\n}\n\nconst money = (value: number): string => `$${value.toLocaleString()}`;\n\nexport function AssetsAssetCategoriesPage() {\n const navigate = useAssetHandlerNavigate();\n const term = useAssetTerminology();\n const hiddenCategories = useHiddenCategoryIds();\n const { labels: categoryLabels } = useAssetCategoryPresentation();\n const { assets: allAssets } = useAssetHandlerStore();\n const assets = useMemo(\n () => allAssets.filter((a) => !hiddenCategories.has(a.category)),\n [allAssets, hiddenCategories]\n );\n const [chartMetric, setChartMetric] = useState<'count' | 'value'>('count');\n const [sortKey, setSortKey] = useState<'count' | 'value'>('count');\n\n const aggregates = useMemo<CategoryAggregate[]>(() => {\n const map: Record<string, CategoryAggregate> = {};\n for (const asset of assets) {\n if (!map[asset.category]) {\n map[asset.category] = {\n key: asset.category,\n count: 0,\n totalValue: 0,\n statuses: {},\n conditions: {},\n avgCondition: 0,\n };\n }\n const cat = map[asset.category];\n cat.count += 1;\n cat.totalValue += asset.currentValue ?? asset.purchaseValue ?? 0;\n cat.statuses[asset.status] = (cat.statuses[asset.status] || 0) + 1;\n cat.conditions[asset.condition] = (cat.conditions[asset.condition] || 0) + 1;\n }\n for (const cat of Object.values(map)) {\n let scoreSum = 0;\n let scoreN = 0;\n for (const [cond, n] of Object.entries(cat.conditions)) {\n const score = CONDITION_SCORE[cond];\n if (score) {\n scoreSum += score * n;\n scoreN += n;\n }\n }\n cat.avgCondition = scoreN > 0 ? scoreSum / scoreN : 0;\n }\n return Object.values(map).sort((a, b) => b.count - a.count);\n }, [assets]);\n\n const totalAssets = assets.length;\n const totalValue = useMemo(\n () => assets.reduce((sum, a) => sum + (a.currentValue ?? a.purchaseValue ?? 0), 0),\n [assets]\n );\n\n const chartData = useMemo(\n () =>\n aggregates.map((cat) => ({\n category: formatAssetCategory(cat.key, categoryLabels),\n count: cat.count,\n value: cat.totalValue,\n })),\n [aggregates, categoryLabels]\n );\n\n const sortedRows = useMemo(\n () =>\n [...aggregates].sort((a, b) =>\n sortKey === 'count' ? b.count - a.count : b.totalValue - a.totalValue\n ),\n [aggregates, sortKey]\n );\n\n const copyId = (key: string) => {\n void navigator.clipboard?.writeText(key);\n notify.success('Category copied', `\"${formatAssetCategory(key, categoryLabels)}\" copied to clipboard.`);\n };\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\">\n <div>\n <h1 className=\"text-2xl font-bold\">\n <GradientText>{term.asset} Categories</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {aggregates.length} categories · {totalAssets} total {term.assets.toLowerCase()}\n </p>\n </div>\n\n <PagePurpose>\n Group and compare your assets by category to see where value and volume are concentrated.\n Each card rolls up how many assets you hold, their total book value and a proportional\n status mix — so you can plan procurement and replacements by category, not one asset at a\n time.\n </PagePurpose>\n\n {aggregates.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No categories yet\"\n description={`Add ${term.assets.toLowerCase()} and they will be grouped into categories here automatically.`}\n />\n ) : (\n <>\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <StatTile\n label=\"Categories\"\n value={aggregates.length}\n format={formatCompact}\n icon={Layers}\n hero\n />\n <StatTile\n label={`${term.assets} tracked`}\n value={totalAssets}\n format={formatCompact}\n icon={Box}\n />\n <StatTile\n label=\"Total book value\"\n value={totalValue}\n format={formatCurrencyCompact('USD')}\n icon={Box}\n />\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {aggregates.map((cat, idx) => {\n const pct = totalAssets > 0 ? Math.round((cat.count / totalAssets) * 100) : 0;\n const segments = STATUS_SEGMENTS.filter((s) => (cat.statuses[s.key] || 0) > 0);\n return (\n <GlassCard\n key={cat.key}\n treatment={idx === 0 ? 'glass' : 'plain'}\n glow={idx === 0}\n className=\"border-border-seam\"\n >\n <div className=\"space-y-4 p-5\">\n {/* Header: icon + title + overflow */}\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className={`shrink-0 rounded-lg p-2 ${tintFor(cat.key)}`}>\n {categoryIcons[cat.key] || <Box className=\"h-5 w-5\" />}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-base font-semibold capitalize\">\n {formatAssetCategory(cat.key, categoryLabels)}\n </p>\n <Badge variant=\"outline\" className=\"mt-0.5 text-xs\">\n {pct}% of estate\n </Badge>\n </div>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 shrink-0\"\n aria-label={`Actions for ${formatAssetCategory(cat.key, categoryLabels)}`}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => navigate('/assets/asset-list')}>\n <ExternalLink className=\"mr-2 h-4 w-4\" />\n View {term.assets.toLowerCase()}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => copyId(cat.key)}>\n <Copy className=\"mr-2 h-4 w-4\" />\n Copy category ID\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n\n {/* Dual metric row */}\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <div>\n <p className=\"text-xs text-text-secondary\">{term.asset} count</p>\n <p className=\"text-2xl font-bold tabular-nums\">{cat.count}</p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-xs text-text-secondary\">Total value</p>\n <p className=\"text-2xl font-bold tabular-nums\">\n {formatCurrencyCompact('USD')(cat.totalValue)}\n </p>\n </div>\n </div>\n\n {/* Proportional status bar */}\n <div>\n <div\n className=\"flex h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"img\"\n aria-label={segments\n .map((s) => `${s.label}: ${cat.statuses[s.key]}`)\n .join(', ')}\n >\n {segments.map((s) => (\n <div\n key={s.key}\n style={{\n width: `${((cat.statuses[s.key] || 0) / cat.count) * 100}%`,\n backgroundColor: s.varName,\n }}\n />\n ))}\n </div>\n <div className=\"mt-2 flex flex-wrap gap-x-3 gap-y-1\">\n {segments.map((s) => (\n <span\n key={s.key}\n className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary\"\n >\n <span\n className=\"h-2.5 w-2.5 rounded-[3px]\"\n style={{ backgroundColor: s.varName }}\n />\n {s.label}\n <span className=\"tabular-nums text-text-muted\">\n {cat.statuses[s.key]}\n </span>\n </span>\n ))}\n </div>\n </div>\n\n {/* Avg condition */}\n <div className=\"flex items-center justify-between border-t border-border-seam pt-3 text-sm\">\n <span className=\"text-text-secondary\">Avg. condition</span>\n <span className=\"font-semibold tabular-nums\">\n {cat.avgCondition > 0 ? cat.avgCondition.toFixed(1) : '—'}\n <span className=\"text-xs font-normal text-text-muted\"> / 5</span>\n </span>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {/* Cross-category comparison chart */}\n <ChartCard\n title=\"Category comparison\"\n subtitle={\n chartMetric === 'count' ? `${term.assets} per category` : 'Book value per category'\n }\n actions={\n <div className=\"flex gap-1 rounded-md border border-border-seam p-0.5\">\n <Button\n variant={chartMetric === 'count' ? 'default' : 'ghost'}\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={() => setChartMetric('count')}\n >\n Count\n </Button>\n <Button\n variant={chartMetric === 'value' ? 'default' : 'ghost'}\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={() => setChartMetric('value')}\n >\n Value\n </Button>\n </div>\n }\n >\n <BarsChart\n data={chartData}\n xKey=\"category\"\n orientation=\"horizontal\"\n height={Math.max(200, chartData.length * 44)}\n categoryWidth={120}\n valueFormat={chartMetric === 'value' ? formatCurrencyCompact('USD') : formatCompact}\n series={[\n {\n key: chartMetric,\n label: chartMetric === 'count' ? term.assets : 'Book value',\n },\n ]}\n />\n </ChartCard>\n\n {/* Summary comparison table */}\n <ChartCard\n title=\"Category summary\"\n subtitle=\"Sortable roll-up across every category\"\n icon={Layers}\n >\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[520px] text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-left text-text-secondary\">\n <th className=\"py-2 pr-4 font-medium\">Category</th>\n <th className=\"py-2 pr-4 font-medium\">\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:text-text-primary\"\n onClick={() => setSortKey('count')}\n >\n Count\n <ArrowUpDown\n className={`h-3 w-3 ${sortKey === 'count' ? 'text-action-primary-bg' : ''}`}\n />\n </button>\n </th>\n <th className=\"py-2 pr-4 font-medium\">\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:text-text-primary\"\n onClick={() => setSortKey('value')}\n >\n Value\n <ArrowUpDown\n className={`h-3 w-3 ${sortKey === 'value' ? 'text-action-primary-bg' : ''}`}\n />\n </button>\n </th>\n <th className=\"py-2 font-medium\">Avg. condition</th>\n </tr>\n </thead>\n <tbody>\n {sortedRows.map((cat) => (\n <tr key={cat.key} className=\"border-b border-border-seam/60 last:border-0\">\n <td className=\"py-2.5 pr-4 font-medium capitalize\">\n {formatAssetCategory(cat.key, categoryLabels)}\n </td>\n <td className=\"py-2.5 pr-4 tabular-nums\">{cat.count}</td>\n <td className=\"py-2.5 pr-4 tabular-nums\">{money(cat.totalValue)}</td>\n <td className=\"py-2.5 tabular-nums\">\n {cat.avgCondition > 0 ? cat.avgCondition.toFixed(1) : '—'}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </ChartCard>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|