@burdenoff/microfe-assethandler 2026.1103.2 → 2026.1107.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/admin/AdminPage.d.ts.map +1 -1
- package/dist/assethandler/admin/AdminPage.js +122 -186
- package/dist/assethandler/admin/AdminPage.js.map +1 -1
- package/dist/assethandler/admin/adminOperations.d.ts +19 -5
- package/dist/assethandler/admin/adminOperations.d.ts.map +1 -1
- package/dist/assethandler/admin/adminOperations.js +23 -20
- package/dist/assethandler/admin/adminOperations.js.map +1 -1
- package/dist/assethandler/pages/FieldModePage.d.ts +2 -0
- package/dist/assethandler/pages/FieldModePage.d.ts.map +1 -1
- package/dist/assethandler/pages/FieldModePage.js +45 -40
- package/dist/assethandler/pages/FieldModePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/admin/AdminPage.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"AdminPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/admin/AdminPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAY,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAkD1C,eAAO,MAAM,SAAS,EAAE,EA2NvB,CAAC"}
|
|
@@ -2,178 +2,161 @@ import { StatTile as e } from "../components/StatTile.js";
|
|
|
2
2
|
import { SectionHeader as t } from "../components/SectionHeader.js";
|
|
3
3
|
import "../components/index.js";
|
|
4
4
|
import { useAssetHandlerAdmin as n } from "./AdminContext.js";
|
|
5
|
-
import { ADMIN_FEATURE_FLAGS as r, ADMIN_QUOTA_OVERVIEW as i, ADMIN_WORKSPACE_STATE as a,
|
|
6
|
-
import { useState as
|
|
7
|
-
import { Boxes as
|
|
8
|
-
import { useMutation as
|
|
9
|
-
import { jsx as
|
|
10
|
-
import { Button as
|
|
5
|
+
import { ADMIN_FEATURE_FLAGS as r, ADMIN_QUOTA_OVERVIEW as i, ADMIN_WORKSPACE_STATE as a, REMOVE_FEATURE_FLAG_OVERRIDE as o, SET_FEATURE_FLAG_OVERRIDE as s, summarizeQuotas as c } from "./adminOperations.js";
|
|
6
|
+
import { useState as l } from "react";
|
|
7
|
+
import { Boxes as u, CreditCard as d, Flag as f, Gauge as p, LifeBuoy as m, ShieldAlert as h } from "lucide-react";
|
|
8
|
+
import { useMutation as g, useQuery as _ } from "@apollo/client/react";
|
|
9
|
+
import { jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
10
|
+
import { Button as b, GlassCard as x } from "@burdenoff/fe-libs/ui";
|
|
11
11
|
//#region src/assethandler/admin/AdminPage.tsx
|
|
12
|
-
var
|
|
12
|
+
var S = [
|
|
13
13
|
{
|
|
14
14
|
path: "/admin/billing",
|
|
15
15
|
label: "Billing & plan assignment",
|
|
16
16
|
description: "View the workspace billing account and change its plan.",
|
|
17
|
-
icon:
|
|
17
|
+
icon: d
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
path: "/support",
|
|
21
21
|
label: "User support",
|
|
22
22
|
description: "Support tickets and user assistance for this workspace.",
|
|
23
|
-
icon:
|
|
23
|
+
icon: m
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
path: "/products/feature-flags",
|
|
27
27
|
label: "All feature flags",
|
|
28
28
|
description: "Platform-wide feature-flag management.",
|
|
29
|
-
icon:
|
|
29
|
+
icon: f
|
|
30
30
|
}
|
|
31
31
|
];
|
|
32
|
-
function
|
|
32
|
+
function C(e) {
|
|
33
33
|
return e instanceof Error ? e.message : "Request failed";
|
|
34
34
|
}
|
|
35
|
-
var
|
|
36
|
-
let { workspaceId:
|
|
37
|
-
variables:
|
|
38
|
-
skip:
|
|
39
|
-
}),
|
|
40
|
-
variables:
|
|
41
|
-
skip:
|
|
42
|
-
}),
|
|
43
|
-
variables:
|
|
44
|
-
skip:
|
|
45
|
-
}), [
|
|
46
|
-
if (!
|
|
35
|
+
var w = () => {
|
|
36
|
+
let { workspaceId: d = "", navigate: m } = n(), [w, T] = l(null), E = !d, D = { workspaceId: d }, O = _(a, {
|
|
37
|
+
variables: D,
|
|
38
|
+
skip: E
|
|
39
|
+
}), k = _(r, {
|
|
40
|
+
variables: D,
|
|
41
|
+
skip: E
|
|
42
|
+
}), A = _(i, {
|
|
43
|
+
variables: D,
|
|
44
|
+
skip: E
|
|
45
|
+
}), [j, { loading: M }] = g(s), [N, { loading: P }] = g(o);
|
|
46
|
+
if (!d) return /* @__PURE__ */ y("div", {
|
|
47
47
|
className: "flex min-h-[40vh] items-center justify-center text-text-secondary",
|
|
48
|
-
children: [/* @__PURE__ */
|
|
48
|
+
children: [/* @__PURE__ */ v(h, { className: "mr-2 h-5 w-5" }), "Workspace context is required."]
|
|
49
49
|
});
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
].filter(Boolean),
|
|
50
|
+
let F = O.data?.assetHandlerAdminWorkspaceState, I = k.data?.assetHandlerFeatureFlags.items ?? [], L = c(A.data?.assetHandlerWorkspaceQuotaOverview.assignments ?? []), R = [
|
|
51
|
+
O.error,
|
|
52
|
+
k.error,
|
|
53
|
+
A.error
|
|
54
|
+
].filter(Boolean), z = async (e, t) => {
|
|
55
55
|
try {
|
|
56
|
-
await
|
|
57
|
-
workspaceId:
|
|
56
|
+
await j({ variables: { input: {
|
|
57
|
+
workspaceId: d,
|
|
58
58
|
key: e,
|
|
59
59
|
enabled: t
|
|
60
|
-
} } }), await
|
|
60
|
+
} } }), await k.refetch(), T({
|
|
61
61
|
tone: "info",
|
|
62
62
|
text: `${e} ${t ? "enabled" : "disabled"} for this workspace.`
|
|
63
63
|
});
|
|
64
64
|
} catch (e) {
|
|
65
|
-
|
|
65
|
+
T({
|
|
66
66
|
tone: "error",
|
|
67
|
-
text:
|
|
67
|
+
text: C(e)
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
-
},
|
|
70
|
+
}, B = async (e) => {
|
|
71
71
|
try {
|
|
72
|
-
await
|
|
73
|
-
workspaceId:
|
|
72
|
+
await N({ variables: {
|
|
73
|
+
workspaceId: d,
|
|
74
74
|
key: e
|
|
75
|
-
} }), await
|
|
75
|
+
} }), await k.refetch(), T({
|
|
76
76
|
tone: "info",
|
|
77
77
|
text: `Override for ${e} removed.`
|
|
78
78
|
});
|
|
79
79
|
} catch (e) {
|
|
80
|
-
|
|
81
|
-
tone: "error",
|
|
82
|
-
text: O(e)
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}, ne = async () => {
|
|
86
|
-
if ($) try {
|
|
87
|
-
await K({ variables: { input: {
|
|
88
|
-
workspaceId: m,
|
|
89
|
-
quotaName: j,
|
|
90
|
-
noLimit: F,
|
|
91
|
-
limit: F ? null : Q
|
|
92
|
-
} } }), await V.refetch(), A({
|
|
93
|
-
tone: "info",
|
|
94
|
-
text: `${d(j)} set to ${F ? "unlimited" : String(Q)}.`
|
|
95
|
-
});
|
|
96
|
-
} catch (e) {
|
|
97
|
-
A({
|
|
80
|
+
T({
|
|
98
81
|
tone: "error",
|
|
99
|
-
text:
|
|
82
|
+
text: C(e)
|
|
100
83
|
});
|
|
101
84
|
}
|
|
102
85
|
};
|
|
103
|
-
return /* @__PURE__ */
|
|
86
|
+
return /* @__PURE__ */ y("div", {
|
|
104
87
|
className: "space-y-8 p-6",
|
|
105
88
|
children: [
|
|
106
|
-
/* @__PURE__ */
|
|
89
|
+
/* @__PURE__ */ v(t, {
|
|
107
90
|
hero: !0,
|
|
108
91
|
title: "AssetHandler Admin",
|
|
109
92
|
description: "Workspace state, feature flags, asset limits, and shortcuts to billing and support."
|
|
110
93
|
}),
|
|
111
|
-
|
|
112
|
-
role:
|
|
113
|
-
className:
|
|
114
|
-
children: [
|
|
94
|
+
w && /* @__PURE__ */ y("div", {
|
|
95
|
+
role: w.tone === "error" ? "alert" : "status",
|
|
96
|
+
className: w.tone === "error" ? "rounded-md bg-status-error-subtle px-4 py-3 text-sm text-status-error-text" : "rounded-md bg-status-info-subtle px-4 py-3 text-sm text-status-info-text",
|
|
97
|
+
children: [w.text, /* @__PURE__ */ v("button", {
|
|
115
98
|
type: "button",
|
|
116
99
|
className: "ml-4 underline",
|
|
117
|
-
onClick: () =>
|
|
100
|
+
onClick: () => T(null),
|
|
118
101
|
children: "Dismiss"
|
|
119
102
|
})]
|
|
120
103
|
}),
|
|
121
|
-
|
|
104
|
+
R.length > 0 && /* @__PURE__ */ y("div", {
|
|
122
105
|
role: "alert",
|
|
123
106
|
className: "rounded-md bg-status-error-subtle px-4 py-3 text-sm text-status-error-text",
|
|
124
|
-
children: ["Failed to load admin data. ",
|
|
107
|
+
children: ["Failed to load admin data. ", R.map((e) => e?.message).join("; ")]
|
|
125
108
|
}),
|
|
126
|
-
/* @__PURE__ */
|
|
109
|
+
/* @__PURE__ */ y("section", {
|
|
127
110
|
"aria-labelledby": "ah-admin-state",
|
|
128
|
-
children: [/* @__PURE__ */
|
|
111
|
+
children: [/* @__PURE__ */ v(t, {
|
|
129
112
|
title: "Workspace state",
|
|
130
|
-
icon:
|
|
131
|
-
}), /* @__PURE__ */
|
|
113
|
+
icon: u
|
|
114
|
+
}), /* @__PURE__ */ y("div", {
|
|
132
115
|
id: "ah-admin-state",
|
|
133
116
|
className: "grid grid-cols-2 gap-4 md:grid-cols-5",
|
|
134
117
|
children: [
|
|
135
|
-
/* @__PURE__ */
|
|
118
|
+
/* @__PURE__ */ v(e, {
|
|
136
119
|
label: "Assets",
|
|
137
|
-
value:
|
|
120
|
+
value: F?.assetCount ?? "—"
|
|
138
121
|
}),
|
|
139
|
-
/* @__PURE__ */
|
|
122
|
+
/* @__PURE__ */ v(e, {
|
|
140
123
|
label: "Open incidents",
|
|
141
|
-
value:
|
|
124
|
+
value: F?.openIncidentCount ?? "—"
|
|
142
125
|
}),
|
|
143
|
-
/* @__PURE__ */
|
|
126
|
+
/* @__PURE__ */ v(e, {
|
|
144
127
|
label: "Open work orders",
|
|
145
|
-
value:
|
|
128
|
+
value: F?.openWorkOrderCount ?? "—"
|
|
146
129
|
}),
|
|
147
|
-
/* @__PURE__ */
|
|
130
|
+
/* @__PURE__ */ v(e, {
|
|
148
131
|
label: "Maintenance plans",
|
|
149
|
-
value:
|
|
132
|
+
value: F?.maintenancePlanCount ?? "—"
|
|
150
133
|
}),
|
|
151
|
-
/* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ v(e, {
|
|
152
135
|
label: "Discovery jobs",
|
|
153
|
-
value:
|
|
136
|
+
value: F?.discoveryJobCount ?? "—"
|
|
154
137
|
})
|
|
155
138
|
]
|
|
156
139
|
})]
|
|
157
140
|
}),
|
|
158
|
-
/* @__PURE__ */
|
|
141
|
+
/* @__PURE__ */ y("section", {
|
|
159
142
|
"aria-labelledby": "ah-admin-flags",
|
|
160
|
-
children: [/* @__PURE__ */
|
|
143
|
+
children: [/* @__PURE__ */ v(t, {
|
|
161
144
|
title: "Feature flags",
|
|
162
145
|
description: "Override a flag for this workspace only. Removing the override restores the platform default.",
|
|
163
|
-
icon:
|
|
164
|
-
}), /* @__PURE__ */
|
|
146
|
+
icon: f
|
|
147
|
+
}), /* @__PURE__ */ y(x, {
|
|
165
148
|
className: "divide-y divide-border",
|
|
166
|
-
children: [
|
|
149
|
+
children: [I.length === 0 && !k.loading && /* @__PURE__ */ v("p", {
|
|
167
150
|
className: "p-4 text-sm text-text-secondary",
|
|
168
151
|
children: "No AssetHandler feature flags found."
|
|
169
|
-
}), /* @__PURE__ */
|
|
152
|
+
}), /* @__PURE__ */ v("ul", {
|
|
170
153
|
id: "ah-admin-flags",
|
|
171
|
-
children:
|
|
154
|
+
children: I.map((e) => /* @__PURE__ */ y("li", {
|
|
172
155
|
className: "flex flex-wrap items-center justify-between gap-3 p-4",
|
|
173
|
-
children: [/* @__PURE__ */
|
|
156
|
+
children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("p", {
|
|
174
157
|
className: "font-mono text-sm text-text-primary",
|
|
175
158
|
children: e.key
|
|
176
|
-
}), /* @__PURE__ */
|
|
159
|
+
}), /* @__PURE__ */ y("p", {
|
|
177
160
|
className: "text-xs text-text-secondary",
|
|
178
161
|
children: [
|
|
179
162
|
e.category,
|
|
@@ -181,23 +164,23 @@ var k = () => {
|
|
|
181
164
|
e.defaultValue ? "on" : "off",
|
|
182
165
|
" · currently",
|
|
183
166
|
" ",
|
|
184
|
-
/* @__PURE__ */
|
|
167
|
+
/* @__PURE__ */ v("strong", { children: e.value ? "on" : "off" })
|
|
185
168
|
]
|
|
186
|
-
})] }), /* @__PURE__ */
|
|
169
|
+
})] }), /* @__PURE__ */ y("div", {
|
|
187
170
|
className: "flex gap-2",
|
|
188
|
-
children: [/* @__PURE__ */
|
|
171
|
+
children: [/* @__PURE__ */ v(b, {
|
|
189
172
|
type: "button",
|
|
190
173
|
size: "sm",
|
|
191
174
|
variant: "outline",
|
|
192
|
-
disabled:
|
|
193
|
-
onClick: () => void
|
|
175
|
+
disabled: M,
|
|
176
|
+
onClick: () => void z(e.key, !e.value),
|
|
194
177
|
children: e.value ? "Disable" : "Enable"
|
|
195
|
-
}), /* @__PURE__ */
|
|
178
|
+
}), /* @__PURE__ */ v(b, {
|
|
196
179
|
type: "button",
|
|
197
180
|
size: "sm",
|
|
198
181
|
variant: "ghost",
|
|
199
|
-
disabled:
|
|
200
|
-
onClick: () => void
|
|
182
|
+
disabled: P,
|
|
183
|
+
onClick: () => void B(e.key),
|
|
201
184
|
children: "Reset to default"
|
|
202
185
|
})]
|
|
203
186
|
})]
|
|
@@ -205,144 +188,97 @@ var k = () => {
|
|
|
205
188
|
})]
|
|
206
189
|
})]
|
|
207
190
|
}),
|
|
208
|
-
/* @__PURE__ */
|
|
191
|
+
/* @__PURE__ */ y("section", {
|
|
209
192
|
"aria-labelledby": "ah-admin-quotas",
|
|
210
|
-
children: [/* @__PURE__ */
|
|
193
|
+
children: [/* @__PURE__ */ v(t, {
|
|
211
194
|
title: "Workspace limits",
|
|
212
|
-
description: "
|
|
213
|
-
icon:
|
|
214
|
-
}), /* @__PURE__ */
|
|
195
|
+
description: "The limits this workspace is held to, including the active-asset limit. Read-only: limits are a billing entitlement and are changed through billing.",
|
|
196
|
+
icon: p
|
|
197
|
+
}), /* @__PURE__ */ y(x, {
|
|
215
198
|
className: "space-y-4 p-4",
|
|
216
|
-
children: [/* @__PURE__ */
|
|
199
|
+
children: [/* @__PURE__ */ y("table", {
|
|
217
200
|
id: "ah-admin-quotas",
|
|
218
201
|
className: "w-full text-left text-sm",
|
|
219
|
-
children: [/* @__PURE__ */
|
|
202
|
+
children: [/* @__PURE__ */ v("thead", {
|
|
220
203
|
className: "text-text-secondary",
|
|
221
|
-
children: /* @__PURE__ */
|
|
222
|
-
/* @__PURE__ */
|
|
204
|
+
children: /* @__PURE__ */ y("tr", { children: [
|
|
205
|
+
/* @__PURE__ */ v("th", {
|
|
223
206
|
className: "py-2",
|
|
224
207
|
children: "Quota"
|
|
225
208
|
}),
|
|
226
|
-
/* @__PURE__ */
|
|
209
|
+
/* @__PURE__ */ v("th", {
|
|
227
210
|
className: "py-2",
|
|
228
211
|
children: "Limit"
|
|
229
212
|
}),
|
|
230
|
-
/* @__PURE__ */
|
|
213
|
+
/* @__PURE__ */ v("th", {
|
|
231
214
|
className: "py-2",
|
|
232
215
|
children: "Used"
|
|
233
216
|
}),
|
|
234
|
-
/* @__PURE__ */
|
|
217
|
+
/* @__PURE__ */ v("th", {
|
|
235
218
|
className: "py-2",
|
|
236
219
|
children: "Resets"
|
|
237
220
|
})
|
|
238
221
|
] })
|
|
239
|
-
}), /* @__PURE__ */
|
|
222
|
+
}), /* @__PURE__ */ y("tbody", { children: [L.length === 0 && /* @__PURE__ */ v("tr", { children: /* @__PURE__ */ v("td", {
|
|
240
223
|
colSpan: 4,
|
|
241
224
|
className: "py-3 text-text-secondary",
|
|
242
225
|
children: "No quota assignments yet — limits come from the workspace plan."
|
|
243
|
-
}) }),
|
|
244
|
-
/* @__PURE__ */
|
|
226
|
+
}) }), L.map((e) => /* @__PURE__ */ y("tr", { children: [
|
|
227
|
+
/* @__PURE__ */ v("td", {
|
|
245
228
|
className: "py-2",
|
|
246
|
-
children:
|
|
229
|
+
children: e.label
|
|
247
230
|
}),
|
|
248
|
-
/* @__PURE__ */
|
|
231
|
+
/* @__PURE__ */ v("td", {
|
|
249
232
|
className: "py-2",
|
|
250
|
-
children: e.
|
|
233
|
+
children: e.unlimited ? "Unlimited" : e.limit
|
|
251
234
|
}),
|
|
252
|
-
/* @__PURE__ */
|
|
235
|
+
/* @__PURE__ */ v("td", {
|
|
253
236
|
className: "py-2",
|
|
254
237
|
children: e.used
|
|
255
238
|
}),
|
|
256
|
-
/* @__PURE__ */
|
|
239
|
+
/* @__PURE__ */ v("td", {
|
|
257
240
|
className: "py-2",
|
|
258
241
|
children: e.resetPeriod
|
|
259
242
|
})
|
|
260
|
-
] }, e.
|
|
261
|
-
}), /* @__PURE__ */
|
|
262
|
-
className: "
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
children: "Quota"
|
|
272
|
-
}), /* @__PURE__ */ x("select", {
|
|
273
|
-
id: "ah-quota-name",
|
|
274
|
-
className: "w-full rounded-md border border-border bg-surface px-3 py-2 text-sm",
|
|
275
|
-
value: j,
|
|
276
|
-
onChange: (e) => M(e.target.value),
|
|
277
|
-
children: o.map((e) => /* @__PURE__ */ x("option", {
|
|
278
|
-
value: e.name,
|
|
279
|
-
children: e.label
|
|
280
|
-
}, e.name))
|
|
281
|
-
})]
|
|
282
|
-
}),
|
|
283
|
-
/* @__PURE__ */ S("div", {
|
|
284
|
-
className: "space-y-2",
|
|
285
|
-
children: [/* @__PURE__ */ x(E, {
|
|
286
|
-
htmlFor: "ah-quota-limit",
|
|
287
|
-
children: "Limit"
|
|
288
|
-
}), /* @__PURE__ */ x(T, {
|
|
289
|
-
id: "ah-quota-limit",
|
|
290
|
-
inputMode: "numeric",
|
|
291
|
-
placeholder: "e.g. 500",
|
|
292
|
-
value: N,
|
|
293
|
-
disabled: F,
|
|
294
|
-
onChange: (e) => P(e.target.value)
|
|
295
|
-
})]
|
|
296
|
-
}),
|
|
297
|
-
/* @__PURE__ */ S("div", {
|
|
298
|
-
className: "flex items-end gap-2 pb-2",
|
|
299
|
-
children: [/* @__PURE__ */ x("input", {
|
|
300
|
-
id: "ah-quota-unlimited",
|
|
301
|
-
type: "checkbox",
|
|
302
|
-
checked: F,
|
|
303
|
-
onChange: (e) => I(e.target.checked)
|
|
304
|
-
}), /* @__PURE__ */ x(E, {
|
|
305
|
-
htmlFor: "ah-quota-unlimited",
|
|
306
|
-
children: "Unlimited"
|
|
307
|
-
})]
|
|
308
|
-
}),
|
|
309
|
-
/* @__PURE__ */ x("div", {
|
|
310
|
-
className: "flex items-end",
|
|
311
|
-
children: /* @__PURE__ */ x(C, {
|
|
312
|
-
type: "submit",
|
|
313
|
-
disabled: !$,
|
|
314
|
-
children: q ? "Saving…" : "Save limit"
|
|
315
|
-
})
|
|
316
|
-
})
|
|
317
|
-
]
|
|
243
|
+
] }, e.quotaName))] })]
|
|
244
|
+
}), /* @__PURE__ */ v("div", {
|
|
245
|
+
className: "border-t border-border pt-4",
|
|
246
|
+
children: /* @__PURE__ */ v(b, {
|
|
247
|
+
type: "button",
|
|
248
|
+
size: "sm",
|
|
249
|
+
variant: "outline",
|
|
250
|
+
disabled: !m,
|
|
251
|
+
onClick: () => m?.("/admin/billing"),
|
|
252
|
+
children: "Change limits in billing"
|
|
253
|
+
})
|
|
318
254
|
})]
|
|
319
255
|
})]
|
|
320
256
|
}),
|
|
321
|
-
/* @__PURE__ */
|
|
257
|
+
/* @__PURE__ */ y("section", {
|
|
322
258
|
"aria-labelledby": "ah-admin-links",
|
|
323
|
-
children: [/* @__PURE__ */
|
|
259
|
+
children: [/* @__PURE__ */ v(t, {
|
|
324
260
|
title: "Billing & support",
|
|
325
261
|
description: "Handled by the shared admin surfaces."
|
|
326
|
-
}), /* @__PURE__ */
|
|
262
|
+
}), /* @__PURE__ */ v("div", {
|
|
327
263
|
id: "ah-admin-links",
|
|
328
264
|
className: "grid gap-4 md:grid-cols-3",
|
|
329
|
-
children:
|
|
265
|
+
children: S.map(({ path: e, label: t, description: n, icon: r }) => /* @__PURE__ */ y(x, {
|
|
330
266
|
className: "space-y-2 p-4",
|
|
331
267
|
children: [
|
|
332
|
-
/* @__PURE__ */
|
|
268
|
+
/* @__PURE__ */ y("p", {
|
|
333
269
|
className: "flex items-center gap-2 font-medium text-text-primary",
|
|
334
|
-
children: [/* @__PURE__ */
|
|
270
|
+
children: [/* @__PURE__ */ v(r, { className: "h-4 w-4" }), t]
|
|
335
271
|
}),
|
|
336
|
-
/* @__PURE__ */
|
|
272
|
+
/* @__PURE__ */ v("p", {
|
|
337
273
|
className: "text-sm text-text-secondary",
|
|
338
274
|
children: n
|
|
339
275
|
}),
|
|
340
|
-
/* @__PURE__ */
|
|
276
|
+
/* @__PURE__ */ v(b, {
|
|
341
277
|
type: "button",
|
|
342
278
|
size: "sm",
|
|
343
279
|
variant: "outline",
|
|
344
|
-
disabled: !
|
|
345
|
-
onClick: () =>
|
|
280
|
+
disabled: !m,
|
|
281
|
+
onClick: () => m?.(e),
|
|
346
282
|
children: "Open"
|
|
347
283
|
})
|
|
348
284
|
]
|
|
@@ -353,6 +289,6 @@ var k = () => {
|
|
|
353
289
|
});
|
|
354
290
|
};
|
|
355
291
|
//#endregion
|
|
356
|
-
export {
|
|
292
|
+
export { w as AdminPage };
|
|
357
293
|
|
|
358
294
|
//# sourceMappingURL=AdminPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminPage.js","names":[],"sources":["../../../src/assethandler/admin/AdminPage.tsx"],"sourcesContent":["/**\n * AssetHandler Admin Page\n *\n * Product-specific governance surface mounted inside `admin-app` at\n * `/admin/assethandler`: workspace state, feature-flag overrides, workspace\n * quota (asset limit) overrides, and shortcuts to the shared admin surfaces for\n * billing plan assignment and user support. All writes are super-admin only\n * (enforced by `wspace-assethandler-svc`).\n */\nimport { useState, type FC } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Boxes, CreditCard, Flag, Gauge, LifeBuoy, ShieldAlert } from 'lucide-react';\nimport { Button, GlassCard, Input, Label } from '@burdenoff/fe-libs/ui';\n\nimport { SectionHeader, StatTile } from '../components';\nimport {\n ADMIN_FEATURE_FLAGS,\n ADMIN_QUOTA_OVERVIEW,\n ADMIN_WORKSPACE_STATE,\n ASSETHANDLER_QUOTAS,\n OVERRIDE_QUOTA,\n REMOVE_FEATURE_FLAG_OVERRIDE,\n SET_FEATURE_FLAG_OVERRIDE,\n parseQuotaLimit,\n quotaLabel,\n type AdminFeatureFlagsData,\n type AdminQuotaOverviewData,\n type AdminWorkspaceStateData,\n} from './adminOperations';\nimport { useAssetHandlerAdmin } from './AdminContext';\n\n/** Shared admin surfaces that already cover billing and support actions. */\nconst SHARED_ADMIN_LINKS: ReadonlyArray<{\n path: string;\n label: string;\n description: string;\n icon: typeof Flag;\n}> = [\n {\n path: '/admin/billing',\n label: 'Billing & plan assignment',\n description: 'View the workspace billing account and change its plan.',\n icon: CreditCard,\n },\n {\n path: '/support',\n label: 'User support',\n description: 'Support tickets and user assistance for this workspace.',\n icon: LifeBuoy,\n },\n {\n path: '/products/feature-flags',\n label: 'All feature flags',\n description: 'Platform-wide feature-flag management.',\n icon: Flag,\n },\n];\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : 'Request failed';\n}\n\nexport const AdminPage: FC = () => {\n const { workspaceId = '', navigate } = useAssetHandlerAdmin();\n const [message, setMessage] = useState<{ tone: 'info' | 'error'; text: string } | null>(null);\n const [quotaName, setQuotaName] = useState(ASSETHANDLER_QUOTAS[0]?.name ?? '');\n const [quotaLimit, setQuotaLimit] = useState('');\n const [quotaUnlimited, setQuotaUnlimited] = useState(false);\n\n const skip = !workspaceId;\n const variables = { workspaceId };\n\n const state = useQuery<AdminWorkspaceStateData>(ADMIN_WORKSPACE_STATE, { variables, skip });\n const flags = useQuery<AdminFeatureFlagsData>(ADMIN_FEATURE_FLAGS, { variables, skip });\n const quotas = useQuery<AdminQuotaOverviewData>(ADMIN_QUOTA_OVERVIEW, { variables, skip });\n\n const [setFlag, { loading: settingFlag }] = useMutation(SET_FEATURE_FLAG_OVERRIDE);\n const [removeFlag, { loading: removingFlag }] = useMutation(REMOVE_FEATURE_FLAG_OVERRIDE);\n const [overrideQuota, { loading: savingQuota }] = useMutation(OVERRIDE_QUOTA);\n\n if (!workspaceId) {\n return (\n <div className=\"flex min-h-[40vh] items-center justify-center text-text-secondary\">\n <ShieldAlert className=\"mr-2 h-5 w-5\" />\n Workspace context is required.\n </div>\n );\n }\n\n const workspaceState = state.data?.assetHandlerAdminWorkspaceState;\n const flagItems = flags.data?.assetHandlerFeatureFlags.items ?? [];\n const assignments = quotas.data?.assetHandlerWorkspaceQuotaOverview.assignments ?? [];\n const loadErrors = [state.error, flags.error, quotas.error].filter(Boolean);\n const parsedLimit = parseQuotaLimit(quotaLimit);\n const canSaveQuota =\n !savingQuota && Boolean(quotaName) && (quotaUnlimited || parsedLimit !== null);\n\n const toggleFlag = async (key: string, enabled: boolean): Promise<void> => {\n try {\n await setFlag({ variables: { input: { workspaceId, key, enabled } } });\n await flags.refetch();\n setMessage({\n tone: 'info',\n text: `${key} ${enabled ? 'enabled' : 'disabled'} for this workspace.`,\n });\n } catch (error) {\n setMessage({ tone: 'error', text: errorMessage(error) });\n }\n };\n\n const clearOverride = async (key: string): Promise<void> => {\n try {\n await removeFlag({ variables: { workspaceId, key } });\n await flags.refetch();\n setMessage({ tone: 'info', text: `Override for ${key} removed.` });\n } catch (error) {\n setMessage({ tone: 'error', text: errorMessage(error) });\n }\n };\n\n const saveQuota = async (): Promise<void> => {\n if (!canSaveQuota) return;\n try {\n await overrideQuota({\n variables: {\n input: {\n workspaceId,\n quotaName,\n noLimit: quotaUnlimited,\n limit: quotaUnlimited ? null : parsedLimit,\n },\n },\n });\n await quotas.refetch();\n setMessage({\n tone: 'info',\n text: `${quotaLabel(quotaName)} set to ${quotaUnlimited ? 'unlimited' : String(parsedLimit)}.`,\n });\n } catch (error) {\n setMessage({ tone: 'error', text: errorMessage(error) });\n }\n };\n\n return (\n <div className=\"space-y-8 p-6\">\n <SectionHeader\n hero\n title=\"AssetHandler Admin\"\n description=\"Workspace state, feature flags, asset limits, and shortcuts to billing and support.\"\n />\n\n {message && (\n <div\n role={message.tone === 'error' ? 'alert' : 'status'}\n className={\n message.tone === 'error'\n ? 'rounded-md bg-status-error-subtle px-4 py-3 text-sm text-status-error-text'\n : 'rounded-md bg-status-info-subtle px-4 py-3 text-sm text-status-info-text'\n }\n >\n {message.text}\n <button type=\"button\" className=\"ml-4 underline\" onClick={() => setMessage(null)}>\n Dismiss\n </button>\n </div>\n )}\n\n {loadErrors.length > 0 && (\n <div\n role=\"alert\"\n className=\"rounded-md bg-status-error-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n Failed to load admin data. {loadErrors.map((error) => error?.message).join('; ')}\n </div>\n )}\n\n <section aria-labelledby=\"ah-admin-state\">\n <SectionHeader title=\"Workspace state\" icon={Boxes} />\n <div id=\"ah-admin-state\" className=\"grid grid-cols-2 gap-4 md:grid-cols-5\">\n <StatTile label=\"Assets\" value={workspaceState?.assetCount ?? '—'} />\n <StatTile label=\"Open incidents\" value={workspaceState?.openIncidentCount ?? '—'} />\n <StatTile label=\"Open work orders\" value={workspaceState?.openWorkOrderCount ?? '—'} />\n <StatTile label=\"Maintenance plans\" value={workspaceState?.maintenancePlanCount ?? '—'} />\n <StatTile label=\"Discovery jobs\" value={workspaceState?.discoveryJobCount ?? '—'} />\n </div>\n </section>\n\n <section aria-labelledby=\"ah-admin-flags\">\n <SectionHeader\n title=\"Feature flags\"\n description=\"Override a flag for this workspace only. Removing the override restores the platform default.\"\n icon={Flag}\n />\n <GlassCard className=\"divide-y divide-border\">\n {flagItems.length === 0 && !flags.loading && (\n <p className=\"p-4 text-sm text-text-secondary\">No AssetHandler feature flags found.</p>\n )}\n <ul id=\"ah-admin-flags\">\n {flagItems.map((flag) => (\n <li key={flag.key} className=\"flex flex-wrap items-center justify-between gap-3 p-4\">\n <div>\n <p className=\"font-mono text-sm text-text-primary\">{flag.key}</p>\n <p className=\"text-xs text-text-secondary\">\n {flag.category} · default {flag.defaultValue ? 'on' : 'off'} · currently{' '}\n <strong>{flag.value ? 'on' : 'off'}</strong>\n </p>\n </div>\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={settingFlag}\n onClick={() => void toggleFlag(flag.key, !flag.value)}\n >\n {flag.value ? 'Disable' : 'Enable'}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n disabled={removingFlag}\n onClick={() => void clearOverride(flag.key)}\n >\n Reset to default\n </Button>\n </div>\n </li>\n ))}\n </ul>\n </GlassCard>\n </section>\n\n <section aria-labelledby=\"ah-admin-quotas\">\n <SectionHeader\n title=\"Workspace limits\"\n description=\"Quota assignments for this workspace, including the active-asset limit.\"\n icon={Gauge}\n />\n <GlassCard className=\"space-y-4 p-4\">\n <table id=\"ah-admin-quotas\" className=\"w-full text-left text-sm\">\n <thead className=\"text-text-secondary\">\n <tr>\n <th className=\"py-2\">Quota</th>\n <th className=\"py-2\">Limit</th>\n <th className=\"py-2\">Used</th>\n <th className=\"py-2\">Resets</th>\n </tr>\n </thead>\n <tbody>\n {assignments.length === 0 && (\n <tr>\n <td colSpan={4} className=\"py-3 text-text-secondary\">\n No quota assignments yet — limits come from the workspace plan.\n </td>\n </tr>\n )}\n {assignments.map((assignment) => (\n <tr key={assignment.id}>\n <td className=\"py-2\">{quotaLabel(assignment.quotaName)}</td>\n <td className=\"py-2\">\n {assignment.noLimit ? 'Unlimited' : (assignment.limit ?? '—')}\n </td>\n <td className=\"py-2\">{assignment.used}</td>\n <td className=\"py-2\">{assignment.resetPeriod}</td>\n </tr>\n ))}\n </tbody>\n </table>\n\n <form\n className=\"grid gap-4 border-t border-border pt-4 md:grid-cols-4\"\n onSubmit={(event) => {\n event.preventDefault();\n void saveQuota();\n }}\n >\n <div className=\"space-y-2\">\n <Label htmlFor=\"ah-quota-name\">Quota</Label>\n <select\n id=\"ah-quota-name\"\n className=\"w-full rounded-md border border-border bg-surface px-3 py-2 text-sm\"\n value={quotaName}\n onChange={(event) => setQuotaName(event.target.value)}\n >\n {ASSETHANDLER_QUOTAS.map((quota) => (\n <option key={quota.name} value={quota.name}>\n {quota.label}\n </option>\n ))}\n </select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"ah-quota-limit\">Limit</Label>\n <Input\n id=\"ah-quota-limit\"\n inputMode=\"numeric\"\n placeholder=\"e.g. 500\"\n value={quotaLimit}\n disabled={quotaUnlimited}\n onChange={(event) => setQuotaLimit(event.target.value)}\n />\n </div>\n <div className=\"flex items-end gap-2 pb-2\">\n <input\n id=\"ah-quota-unlimited\"\n type=\"checkbox\"\n checked={quotaUnlimited}\n onChange={(event) => setQuotaUnlimited(event.target.checked)}\n />\n <Label htmlFor=\"ah-quota-unlimited\">Unlimited</Label>\n </div>\n <div className=\"flex items-end\">\n <Button type=\"submit\" disabled={!canSaveQuota}>\n {savingQuota ? 'Saving…' : 'Save limit'}\n </Button>\n </div>\n </form>\n </GlassCard>\n </section>\n\n <section aria-labelledby=\"ah-admin-links\">\n <SectionHeader\n title=\"Billing & support\"\n description=\"Handled by the shared admin surfaces.\"\n />\n <div id=\"ah-admin-links\" className=\"grid gap-4 md:grid-cols-3\">\n {SHARED_ADMIN_LINKS.map(({ path, label, description, icon: Icon }) => (\n <GlassCard key={path} className=\"space-y-2 p-4\">\n <p className=\"flex items-center gap-2 font-medium text-text-primary\">\n <Icon className=\"h-4 w-4\" />\n {label}\n </p>\n <p className=\"text-sm text-text-secondary\">{description}</p>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={!navigate}\n onClick={() => navigate?.(path)}\n >\n Open\n </Button>\n </GlassCard>\n ))}\n </div>\n </section>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdminPage.js","names":[],"sources":["../../../src/assethandler/admin/AdminPage.tsx"],"sourcesContent":["/**\n * AssetHandler Admin Page\n *\n * Product-specific governance surface mounted inside `admin-app` at\n * `/admin/assethandler`: workspace state, feature-flag overrides, the\n * workspace's effective quota limits (read-only), and shortcuts to the shared\n * admin surfaces for billing plan assignment and user support. Flag writes are\n * super-admin only (enforced by `wspace-assethandler-svc`). Limits are a billing\n * entitlement, so they are changed through billing, not from this page.\n */\nimport { useState, type FC } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Boxes, CreditCard, Flag, Gauge, LifeBuoy, ShieldAlert } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { SectionHeader, StatTile } from '../components';\nimport {\n ADMIN_FEATURE_FLAGS,\n ADMIN_QUOTA_OVERVIEW,\n ADMIN_WORKSPACE_STATE,\n REMOVE_FEATURE_FLAG_OVERRIDE,\n SET_FEATURE_FLAG_OVERRIDE,\n summarizeQuotas,\n type AdminFeatureFlagsData,\n type AdminQuotaOverviewData,\n type AdminWorkspaceStateData,\n} from './adminOperations';\nimport { useAssetHandlerAdmin } from './AdminContext';\n\n/** Shared admin surfaces that already cover billing and support actions. */\nconst SHARED_ADMIN_LINKS: ReadonlyArray<{\n path: string;\n label: string;\n description: string;\n icon: typeof Flag;\n}> = [\n {\n path: '/admin/billing',\n label: 'Billing & plan assignment',\n description: 'View the workspace billing account and change its plan.',\n icon: CreditCard,\n },\n {\n path: '/support',\n label: 'User support',\n description: 'Support tickets and user assistance for this workspace.',\n icon: LifeBuoy,\n },\n {\n path: '/products/feature-flags',\n label: 'All feature flags',\n description: 'Platform-wide feature-flag management.',\n icon: Flag,\n },\n];\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : 'Request failed';\n}\n\nexport const AdminPage: FC = () => {\n const { workspaceId = '', navigate } = useAssetHandlerAdmin();\n const [message, setMessage] = useState<{ tone: 'info' | 'error'; text: string } | null>(null);\n\n const skip = !workspaceId;\n const variables = { workspaceId };\n\n const state = useQuery<AdminWorkspaceStateData>(ADMIN_WORKSPACE_STATE, { variables, skip });\n const flags = useQuery<AdminFeatureFlagsData>(ADMIN_FEATURE_FLAGS, { variables, skip });\n const quotas = useQuery<AdminQuotaOverviewData>(ADMIN_QUOTA_OVERVIEW, { variables, skip });\n\n const [setFlag, { loading: settingFlag }] = useMutation(SET_FEATURE_FLAG_OVERRIDE);\n const [removeFlag, { loading: removingFlag }] = useMutation(REMOVE_FEATURE_FLAG_OVERRIDE);\n\n if (!workspaceId) {\n return (\n <div className=\"flex min-h-[40vh] items-center justify-center text-text-secondary\">\n <ShieldAlert className=\"mr-2 h-5 w-5\" />\n Workspace context is required.\n </div>\n );\n }\n\n const workspaceState = state.data?.assetHandlerAdminWorkspaceState;\n const flagItems = flags.data?.assetHandlerFeatureFlags.items ?? [];\n const quotaLines = summarizeQuotas(\n quotas.data?.assetHandlerWorkspaceQuotaOverview.assignments ?? []\n );\n const loadErrors = [state.error, flags.error, quotas.error].filter(Boolean);\n\n const toggleFlag = async (key: string, enabled: boolean): Promise<void> => {\n try {\n await setFlag({ variables: { input: { workspaceId, key, enabled } } });\n await flags.refetch();\n setMessage({\n tone: 'info',\n text: `${key} ${enabled ? 'enabled' : 'disabled'} for this workspace.`,\n });\n } catch (error) {\n setMessage({ tone: 'error', text: errorMessage(error) });\n }\n };\n\n const clearOverride = async (key: string): Promise<void> => {\n try {\n await removeFlag({ variables: { workspaceId, key } });\n await flags.refetch();\n setMessage({ tone: 'info', text: `Override for ${key} removed.` });\n } catch (error) {\n setMessage({ tone: 'error', text: errorMessage(error) });\n }\n };\n\n return (\n <div className=\"space-y-8 p-6\">\n <SectionHeader\n hero\n title=\"AssetHandler Admin\"\n description=\"Workspace state, feature flags, asset limits, and shortcuts to billing and support.\"\n />\n\n {message && (\n <div\n role={message.tone === 'error' ? 'alert' : 'status'}\n className={\n message.tone === 'error'\n ? 'rounded-md bg-status-error-subtle px-4 py-3 text-sm text-status-error-text'\n : 'rounded-md bg-status-info-subtle px-4 py-3 text-sm text-status-info-text'\n }\n >\n {message.text}\n <button type=\"button\" className=\"ml-4 underline\" onClick={() => setMessage(null)}>\n Dismiss\n </button>\n </div>\n )}\n\n {loadErrors.length > 0 && (\n <div\n role=\"alert\"\n className=\"rounded-md bg-status-error-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n Failed to load admin data. {loadErrors.map((error) => error?.message).join('; ')}\n </div>\n )}\n\n <section aria-labelledby=\"ah-admin-state\">\n <SectionHeader title=\"Workspace state\" icon={Boxes} />\n <div id=\"ah-admin-state\" className=\"grid grid-cols-2 gap-4 md:grid-cols-5\">\n <StatTile label=\"Assets\" value={workspaceState?.assetCount ?? '—'} />\n <StatTile label=\"Open incidents\" value={workspaceState?.openIncidentCount ?? '—'} />\n <StatTile label=\"Open work orders\" value={workspaceState?.openWorkOrderCount ?? '—'} />\n <StatTile label=\"Maintenance plans\" value={workspaceState?.maintenancePlanCount ?? '—'} />\n <StatTile label=\"Discovery jobs\" value={workspaceState?.discoveryJobCount ?? '—'} />\n </div>\n </section>\n\n <section aria-labelledby=\"ah-admin-flags\">\n <SectionHeader\n title=\"Feature flags\"\n description=\"Override a flag for this workspace only. Removing the override restores the platform default.\"\n icon={Flag}\n />\n <GlassCard className=\"divide-y divide-border\">\n {flagItems.length === 0 && !flags.loading && (\n <p className=\"p-4 text-sm text-text-secondary\">No AssetHandler feature flags found.</p>\n )}\n <ul id=\"ah-admin-flags\">\n {flagItems.map((flag) => (\n <li key={flag.key} className=\"flex flex-wrap items-center justify-between gap-3 p-4\">\n <div>\n <p className=\"font-mono text-sm text-text-primary\">{flag.key}</p>\n <p className=\"text-xs text-text-secondary\">\n {flag.category} · default {flag.defaultValue ? 'on' : 'off'} · currently{' '}\n <strong>{flag.value ? 'on' : 'off'}</strong>\n </p>\n </div>\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={settingFlag}\n onClick={() => void toggleFlag(flag.key, !flag.value)}\n >\n {flag.value ? 'Disable' : 'Enable'}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n disabled={removingFlag}\n onClick={() => void clearOverride(flag.key)}\n >\n Reset to default\n </Button>\n </div>\n </li>\n ))}\n </ul>\n </GlassCard>\n </section>\n\n <section aria-labelledby=\"ah-admin-quotas\">\n <SectionHeader\n title=\"Workspace limits\"\n description=\"The limits this workspace is held to, including the active-asset limit. Read-only: limits are a billing entitlement and are changed through billing.\"\n icon={Gauge}\n />\n <GlassCard className=\"space-y-4 p-4\">\n <table id=\"ah-admin-quotas\" className=\"w-full text-left text-sm\">\n <thead className=\"text-text-secondary\">\n <tr>\n <th className=\"py-2\">Quota</th>\n <th className=\"py-2\">Limit</th>\n <th className=\"py-2\">Used</th>\n <th className=\"py-2\">Resets</th>\n </tr>\n </thead>\n <tbody>\n {quotaLines.length === 0 && (\n <tr>\n <td colSpan={4} className=\"py-3 text-text-secondary\">\n No quota assignments yet — limits come from the workspace plan.\n </td>\n </tr>\n )}\n {quotaLines.map((line) => (\n <tr key={line.quotaName}>\n <td className=\"py-2\">{line.label}</td>\n <td className=\"py-2\">{line.unlimited ? 'Unlimited' : line.limit}</td>\n <td className=\"py-2\">{line.used}</td>\n <td className=\"py-2\">{line.resetPeriod}</td>\n </tr>\n ))}\n </tbody>\n </table>\n <div className=\"border-t border-border pt-4\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={!navigate}\n onClick={() => navigate?.('/admin/billing')}\n >\n Change limits in billing\n </Button>\n </div>\n </GlassCard>\n </section>\n\n <section aria-labelledby=\"ah-admin-links\">\n <SectionHeader\n title=\"Billing & support\"\n description=\"Handled by the shared admin surfaces.\"\n />\n <div id=\"ah-admin-links\" className=\"grid gap-4 md:grid-cols-3\">\n {SHARED_ADMIN_LINKS.map(({ path, label, description, icon: Icon }) => (\n <GlassCard key={path} className=\"space-y-2 p-4\">\n <p className=\"flex items-center gap-2 font-medium text-text-primary\">\n <Icon className=\"h-4 w-4\" />\n {label}\n </p>\n <p className=\"text-sm text-text-secondary\">{description}</p>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={!navigate}\n onClick={() => navigate?.(path)}\n >\n Open\n </Button>\n </GlassCard>\n ))}\n </div>\n </section>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -3,7 +3,6 @@ export declare const ADMIN_FEATURE_FLAGS: import('graphql').DocumentNode;
|
|
|
3
3
|
export declare const ADMIN_QUOTA_OVERVIEW: import('graphql').DocumentNode;
|
|
4
4
|
export declare const SET_FEATURE_FLAG_OVERRIDE: import('graphql').DocumentNode;
|
|
5
5
|
export declare const REMOVE_FEATURE_FLAG_OVERRIDE: import('graphql').DocumentNode;
|
|
6
|
-
export declare const OVERRIDE_QUOTA: import('graphql').DocumentNode;
|
|
7
6
|
export interface AdminWorkspaceState {
|
|
8
7
|
workspaceId: string;
|
|
9
8
|
assetCount: number;
|
|
@@ -52,10 +51,25 @@ export declare const ASSETHANDLER_QUOTAS: ReadonlyArray<{
|
|
|
52
51
|
}>;
|
|
53
52
|
/** Human label for a quota name; falls back to the raw name for unknown ones. */
|
|
54
53
|
export declare function quotaLabel(name: string): string;
|
|
54
|
+
/** One line per quota: every active plan row for that quota folded together. */
|
|
55
|
+
export interface QuotaSummary {
|
|
56
|
+
quotaName: string;
|
|
57
|
+
label: string;
|
|
58
|
+
/** Effective limit; `null` when the quota is unlimited. */
|
|
59
|
+
limit: number | null;
|
|
60
|
+
unlimited: boolean;
|
|
61
|
+
used: number;
|
|
62
|
+
resetPeriod: string;
|
|
63
|
+
}
|
|
55
64
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* the
|
|
65
|
+
* Fold a workspace's active quota assignments into one line per quota.
|
|
66
|
+
*
|
|
67
|
+
* A workspace can hold several active assignments for the same quota (one per
|
|
68
|
+
* subscription). The quota service enforces their combined total: the limits
|
|
69
|
+
* add up, and any unlimited assignment makes the whole quota unlimited. This
|
|
70
|
+
* mirrors that rule so the admin sees the limit that is actually enforced, not
|
|
71
|
+
* a row per subscription. Quotas AssetHandler knows come first, in a stable
|
|
72
|
+
* order; anything else follows alphabetically.
|
|
59
73
|
*/
|
|
60
|
-
export declare function
|
|
74
|
+
export declare function summarizeQuotas(assignments: readonly AdminQuotaAssignment[]): QuotaSummary[];
|
|
61
75
|
//# sourceMappingURL=adminOperations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adminOperations.d.ts","sourceRoot":"","sources":["../../../src/assethandler/admin/adminOperations.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,qBAAqB,gCAYjC,CAAC;AAEF,eAAO,MAAM,mBAAmB,gCAY/B,CAAC;AAEF,eAAO,MAAM,oBAAoB,gCAiBhC,CAAC;AAEF,eAAO,MAAM,yBAAyB,gCAUrC,CAAC;AAEF,eAAO,MAAM,4BAA4B,gCAIxC,CAAC;AAEF,
|
|
1
|
+
{"version":3,"file":"adminOperations.d.ts","sourceRoot":"","sources":["../../../src/assethandler/admin/adminOperations.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,qBAAqB,gCAYjC,CAAC;AAEF,eAAO,MAAM,mBAAmB,gCAY/B,CAAC;AAEF,eAAO,MAAM,oBAAoB,gCAiBhC,CAAC;AAEF,eAAO,MAAM,yBAAyB,gCAUrC,CAAC;AAEF,eAAO,MAAM,4BAA4B,gCAIxC,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,+BAA+B,EAAE,mBAAmB,CAAC;CACtD;AAED,MAAM,WAAW,qBAAqB;IACpC,wBAAwB,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,gBAAgB,EAAE,CAAA;KAAE,CAAC;CAC9E;AAED,MAAM,WAAW,sBAAsB;IACrC,kCAAkC,EAAE;QAClC,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,oBAAoB,EAAE,CAAC;KACrC,CAAC;CACH;AAED,8DAA8D;AAC9D,eAAO,MAAM,mBAAmB,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAS9E,CAAC;AAEF,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,GAAG,YAAY,EAAE,CA2B5F"}
|
|
@@ -55,18 +55,7 @@ var t = e`
|
|
|
55
55
|
mutation RemoveAssetHandlerFeatureFlagOverride($workspaceId: ID!, $key: String!) {
|
|
56
56
|
removeAssetHandlerFeatureFlagOverride(workspaceId: $workspaceId, key: $key)
|
|
57
57
|
}
|
|
58
|
-
`, o =
|
|
59
|
-
mutation OverrideAssetHandlerWorkspaceQuota($input: OverrideAssetHandlerWorkspaceQuotaInput!) {
|
|
60
|
-
overrideAssetHandlerWorkspaceQuota(input: $input) {
|
|
61
|
-
id
|
|
62
|
-
workspaceId
|
|
63
|
-
quotaName
|
|
64
|
-
limit
|
|
65
|
-
used
|
|
66
|
-
noLimit
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
`, s = [
|
|
58
|
+
`, o = [
|
|
70
59
|
{
|
|
71
60
|
name: "assethandler.assets.count",
|
|
72
61
|
label: "Active assets (asset limit)"
|
|
@@ -100,16 +89,30 @@ var t = e`
|
|
|
100
89
|
label: "ESG metrics / month"
|
|
101
90
|
}
|
|
102
91
|
];
|
|
103
|
-
function
|
|
104
|
-
return
|
|
92
|
+
function s(e) {
|
|
93
|
+
return o.find((t) => t.name === e)?.label ?? e;
|
|
105
94
|
}
|
|
106
|
-
function
|
|
107
|
-
let t =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
95
|
+
function c(e) {
|
|
96
|
+
let t = /* @__PURE__ */ new Map();
|
|
97
|
+
for (let n of e) {
|
|
98
|
+
if (!n.isActive) continue;
|
|
99
|
+
let e = t.get(n.quotaName);
|
|
100
|
+
e ? e.push(n) : t.set(n.quotaName, [n]);
|
|
101
|
+
}
|
|
102
|
+
let n = o.map((e) => e.name);
|
|
103
|
+
return [...n.filter((e) => t.has(e)), ...[...t.keys()].filter((e) => !n.includes(e)).sort()].map((e) => {
|
|
104
|
+
let n = t.get(e) ?? [], r = n.some((e) => e.noLimit || e.limit === null);
|
|
105
|
+
return {
|
|
106
|
+
quotaName: e,
|
|
107
|
+
label: s(e),
|
|
108
|
+
limit: r ? null : n.reduce((e, t) => e + (t.limit ?? 0), 0),
|
|
109
|
+
unlimited: r,
|
|
110
|
+
used: n.reduce((e, t) => e + t.used, 0),
|
|
111
|
+
resetPeriod: [...new Set(n.map((e) => e.resetPeriod))].join(" / ")
|
|
112
|
+
};
|
|
113
|
+
});
|
|
111
114
|
}
|
|
112
115
|
//#endregion
|
|
113
|
-
export { n as ADMIN_FEATURE_FLAGS, r as ADMIN_QUOTA_OVERVIEW, t as ADMIN_WORKSPACE_STATE,
|
|
116
|
+
export { n as ADMIN_FEATURE_FLAGS, r as ADMIN_QUOTA_OVERVIEW, t as ADMIN_WORKSPACE_STATE, a as REMOVE_FEATURE_FLAG_OVERRIDE, i as SET_FEATURE_FLAG_OVERRIDE, c as summarizeQuotas };
|
|
114
117
|
|
|
115
118
|
//# sourceMappingURL=adminOperations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adminOperations.js","names":[],"sources":["../../../src/assethandler/admin/adminOperations.ts"],"sourcesContent":["/**\n * GraphQL operations for the AssetHandler admin surface (BOFF-3362).\n *\n * Served by `wspace-assethandler-svc`'s admin module (super-admin only).\n * Typed locally rather than through codegen so this surface does not depend on\n * the composed supergraph snapshot being refreshed first.\n */\nimport { gql } from '@apollo/client';\n\nexport const ADMIN_WORKSPACE_STATE = gql`\n query AssetHandlerAdminWorkspaceState($workspaceId: ID!) {\n assetHandlerAdminWorkspaceState(workspaceId: $workspaceId) {\n workspaceId\n assetCount\n openIncidentCount\n openWorkOrderCount\n maintenancePlanCount\n discoveryJobCount\n generatedAt\n }\n }\n`;\n\nexport const ADMIN_FEATURE_FLAGS = gql`\n query AssetHandlerFeatureFlags($workspaceId: ID!) {\n assetHandlerFeatureFlags(workspaceId: $workspaceId) {\n workspaceId\n items {\n key\n defaultValue\n value\n category\n }\n }\n }\n`;\n\nexport const ADMIN_QUOTA_OVERVIEW = gql`\n query AssetHandlerWorkspaceQuotaOverview($workspaceId: ID!) {\n assetHandlerWorkspaceQuotaOverview(workspaceId: $workspaceId) {\n workspaceId\n assignments {\n id\n quotaName\n productId\n limit\n used\n noLimit\n resetPeriod\n isActive\n updatedAt\n }\n }\n }\n`;\n\nexport const SET_FEATURE_FLAG_OVERRIDE = gql`\n mutation SetAssetHandlerFeatureFlagOverride($input: SetAssetHandlerFeatureFlagOverrideInput!) {\n setAssetHandlerFeatureFlagOverride(input: $input) {\n id\n featureFlagId\n workspaceId\n enabled\n allowUserOptIn\n }\n }\n`;\n\nexport const REMOVE_FEATURE_FLAG_OVERRIDE = gql`\n mutation RemoveAssetHandlerFeatureFlagOverride($workspaceId: ID!, $key: String!) {\n removeAssetHandlerFeatureFlagOverride(workspaceId: $workspaceId, key: $key)\n }\n`;\n\nexport
|
|
1
|
+
{"version":3,"file":"adminOperations.js","names":[],"sources":["../../../src/assethandler/admin/adminOperations.ts"],"sourcesContent":["/**\n * GraphQL operations for the AssetHandler admin surface (BOFF-3362).\n *\n * Served by `wspace-assethandler-svc`'s admin module (super-admin only).\n * Typed locally rather than through codegen so this surface does not depend on\n * the composed supergraph snapshot being refreshed first.\n */\nimport { gql } from '@apollo/client';\n\nexport const ADMIN_WORKSPACE_STATE = gql`\n query AssetHandlerAdminWorkspaceState($workspaceId: ID!) {\n assetHandlerAdminWorkspaceState(workspaceId: $workspaceId) {\n workspaceId\n assetCount\n openIncidentCount\n openWorkOrderCount\n maintenancePlanCount\n discoveryJobCount\n generatedAt\n }\n }\n`;\n\nexport const ADMIN_FEATURE_FLAGS = gql`\n query AssetHandlerFeatureFlags($workspaceId: ID!) {\n assetHandlerFeatureFlags(workspaceId: $workspaceId) {\n workspaceId\n items {\n key\n defaultValue\n value\n category\n }\n }\n }\n`;\n\nexport const ADMIN_QUOTA_OVERVIEW = gql`\n query AssetHandlerWorkspaceQuotaOverview($workspaceId: ID!) {\n assetHandlerWorkspaceQuotaOverview(workspaceId: $workspaceId) {\n workspaceId\n assignments {\n id\n quotaName\n productId\n limit\n used\n noLimit\n resetPeriod\n isActive\n updatedAt\n }\n }\n }\n`;\n\nexport const SET_FEATURE_FLAG_OVERRIDE = gql`\n mutation SetAssetHandlerFeatureFlagOverride($input: SetAssetHandlerFeatureFlagOverrideInput!) {\n setAssetHandlerFeatureFlagOverride(input: $input) {\n id\n featureFlagId\n workspaceId\n enabled\n allowUserOptIn\n }\n }\n`;\n\nexport const REMOVE_FEATURE_FLAG_OVERRIDE = gql`\n mutation RemoveAssetHandlerFeatureFlagOverride($workspaceId: ID!, $key: String!) {\n removeAssetHandlerFeatureFlagOverride(workspaceId: $workspaceId, key: $key)\n }\n`;\n\nexport interface AdminWorkspaceState {\n workspaceId: string;\n assetCount: number;\n openIncidentCount: number;\n openWorkOrderCount: number;\n maintenancePlanCount: number;\n discoveryJobCount: number;\n generatedAt: string;\n}\n\nexport interface AdminFeatureFlag {\n key: string;\n defaultValue: boolean;\n value: boolean;\n category: string;\n}\n\nexport interface AdminQuotaAssignment {\n id: string;\n quotaName: string;\n productId: string;\n limit: number | null;\n used: number;\n noLimit: boolean;\n resetPeriod: string;\n isActive: boolean;\n updatedAt: string;\n}\n\nexport interface AdminWorkspaceStateData {\n assetHandlerAdminWorkspaceState: AdminWorkspaceState;\n}\n\nexport interface AdminFeatureFlagsData {\n assetHandlerFeatureFlags: { workspaceId: string; items: AdminFeatureFlag[] };\n}\n\nexport interface AdminQuotaOverviewData {\n assetHandlerWorkspaceQuotaOverview: {\n workspaceId: string;\n assignments: AdminQuotaAssignment[];\n };\n}\n\n/** Quotas AssetHandler enforces, with admin-facing labels. */\nexport const ASSETHANDLER_QUOTAS: ReadonlyArray<{ name: string; label: string }> = [\n { name: 'assethandler.assets.count', label: 'Active assets (asset limit)' },\n { name: 'assethandler.work_orders.open', label: 'Open work orders' },\n { name: 'assethandler.cmdb.configuration_items', label: 'CMDB configuration items' },\n { name: 'assethandler.discovery.jobs.monthly', label: 'Discovery jobs / month' },\n { name: 'assethandler.incidents.monthly', label: 'Incidents / month' },\n { name: 'assethandler.maintenance_records.monthly', label: 'Maintenance records / month' },\n { name: 'assethandler.knowledge_articles.monthly', label: 'Knowledge articles / month' },\n { name: 'assethandler.esg_metrics.monthly', label: 'ESG metrics / month' },\n];\n\n/** Human label for a quota name; falls back to the raw name for unknown ones. */\nexport function quotaLabel(name: string): string {\n return ASSETHANDLER_QUOTAS.find((quota) => quota.name === name)?.label ?? name;\n}\n\n/** One line per quota: every active plan row for that quota folded together. */\nexport interface QuotaSummary {\n quotaName: string;\n label: string;\n /** Effective limit; `null` when the quota is unlimited. */\n limit: number | null;\n unlimited: boolean;\n used: number;\n resetPeriod: string;\n}\n\n/**\n * Fold a workspace's active quota assignments into one line per quota.\n *\n * A workspace can hold several active assignments for the same quota (one per\n * subscription). The quota service enforces their combined total: the limits\n * add up, and any unlimited assignment makes the whole quota unlimited. This\n * mirrors that rule so the admin sees the limit that is actually enforced, not\n * a row per subscription. Quotas AssetHandler knows come first, in a stable\n * order; anything else follows alphabetically.\n */\nexport function summarizeQuotas(assignments: readonly AdminQuotaAssignment[]): QuotaSummary[] {\n const byQuota = new Map<string, AdminQuotaAssignment[]>();\n for (const assignment of assignments) {\n if (!assignment.isActive) continue;\n const rows = byQuota.get(assignment.quotaName);\n if (rows) rows.push(assignment);\n else byQuota.set(assignment.quotaName, [assignment]);\n }\n\n const known = ASSETHANDLER_QUOTAS.map((quota) => quota.name);\n const names = [\n ...known.filter((name) => byQuota.has(name)),\n ...[...byQuota.keys()].filter((name) => !known.includes(name)).sort(),\n ];\n\n return names.map((quotaName) => {\n const rows = byQuota.get(quotaName) ?? [];\n const unlimited = rows.some((row) => row.noLimit || row.limit === null);\n return {\n quotaName,\n label: quotaLabel(quotaName),\n limit: unlimited ? null : rows.reduce((sum, row) => sum + (row.limit ?? 0), 0),\n unlimited,\n used: rows.reduce((sum, row) => sum + row.used, 0),\n resetPeriod: [...new Set(rows.map((row) => row.resetPeriod))].join(' / '),\n };\n });\n}\n"],"mappings":";;AASA,IAAa,IAAwB,CAAG;;;;;;;;;;;;GAc3B,IAAsB,CAAG;;;;;;;;;;;;GAczB,IAAuB,CAAG;;;;;;;;;;;;;;;;;GAmB1B,IAA4B,CAAG;;;;;;;;;;GAY/B,IAA+B,CAAG;;;;GAmDlC,IAAsE;CACjF;EAAE,MAAM;EAA6B,OAAO;CAA8B;CAC1E;EAAE,MAAM;EAAiC,OAAO;CAAmB;CACnE;EAAE,MAAM;EAAyC,OAAO;CAA2B;CACnF;EAAE,MAAM;EAAuC,OAAO;CAAyB;CAC/E;EAAE,MAAM;EAAkC,OAAO;CAAoB;CACrE;EAAE,MAAM;EAA4C,OAAO;CAA8B;CACzF;EAAE,MAAM;EAA2C,OAAO;CAA6B;CACvF;EAAE,MAAM;EAAoC,OAAO;CAAsB;AAC3E;AAGA,SAAgB,EAAW,GAAsB;CAC/C,OAAO,EAAoB,MAAM,MAAU,EAAM,SAAS,CAAI,GAAG,SAAS;AAC5E;AAuBA,SAAgB,EAAgB,GAA8D;CAC5F,IAAM,oBAAU,IAAI,IAAoC;CACxD,KAAK,IAAM,KAAc,GAAa;EACpC,IAAI,CAAC,EAAW,UAAU;EAC1B,IAAM,IAAO,EAAQ,IAAI,EAAW,SAAS;EAC7C,AAAI,IAAM,EAAK,KAAK,CAAU,IACzB,EAAQ,IAAI,EAAW,WAAW,CAAC,CAAU,CAAC;CACrD;CAEA,IAAM,IAAQ,EAAoB,KAAK,MAAU,EAAM,IAAI;CAM3D,OAAO,CAJL,GAAG,EAAM,QAAQ,MAAS,EAAQ,IAAI,CAAI,CAAC,GAC3C,GAAG,CAAC,GAAG,EAAQ,KAAK,CAAC,EAAE,QAAQ,MAAS,CAAC,EAAM,SAAS,CAAI,CAAC,EAAE,KAAK,CAG/D,EAAM,KAAK,MAAc;EAC9B,IAAM,IAAO,EAAQ,IAAI,CAAS,KAAK,CAAC,GAClC,IAAY,EAAK,MAAM,MAAQ,EAAI,WAAW,EAAI,UAAU,IAAI;EACtE,OAAO;GACL;GACA,OAAO,EAAW,CAAS;GAC3B,OAAO,IAAY,OAAO,EAAK,QAAQ,GAAK,MAAQ,KAAO,EAAI,SAAS,IAAI,CAAC;GAC7E;GACA,MAAM,EAAK,QAAQ,GAAK,MAAQ,IAAM,EAAI,MAAM,CAAC;GACjD,aAAa,CAAC,GAAG,IAAI,IAAI,EAAK,KAAK,MAAQ,EAAI,WAAW,CAAC,CAAC,EAAE,KAAK,KAAK;EAC1E;CACF,CAAC;AACH"}
|
|
@@ -13,5 +13,7 @@
|
|
|
13
13
|
* would let an offline scan resolve against another workspace's registry.
|
|
14
14
|
*/
|
|
15
15
|
export declare const FIELD_ASSETS: import('graphql').DocumentNode;
|
|
16
|
+
/** Online lookup of one asset by its printed tag (scan or deep link). */
|
|
17
|
+
export declare const FIELD_ASSET_BY_TAG: import('graphql').DocumentNode;
|
|
16
18
|
export declare function FieldModePage(): import("react").JSX.Element;
|
|
17
19
|
//# sourceMappingURL=FieldModePage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FieldModePage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/FieldModePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FieldModePage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/FieldModePage.tsx"],"names":[],"mappings":"AA0CA;;;;;;GAMG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,gCAYxB,CAAC;AAwBF,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,gCAY9B,CAAC;AAsDF,wBAAgB,aAAa,gCAK5B"}
|
|
@@ -11,12 +11,13 @@ import { browserStorage as o, useFieldQueue as ie } from "../hooks/useFieldQueue
|
|
|
11
11
|
import { fieldAssetCacheKey as ae, filterFieldAssets as oe, findFieldAsset as se, loadFieldAssets as ce, normalizeScannedTag as le, saveFieldAssets as ue, toFieldAsset as s } from "../utils/fieldAssetCache.js";
|
|
12
12
|
import { useCallback as c, useEffect as l, useMemo as de, useRef as u, useState as d } from "react";
|
|
13
13
|
import { ArrowDownToLine as fe, ArrowUpFromLine as pe, Camera as me, CloudOff as he, RefreshCw as ge, ScanLine as _e, Smartphone as ve, Trash2 as ye, Wifi as be } from "lucide-react";
|
|
14
|
+
import { useSearchParams as xe } from "react-router";
|
|
14
15
|
import { gql as f } from "@apollo/client";
|
|
15
|
-
import { useApolloClient as
|
|
16
|
+
import { useApolloClient as Se } from "@apollo/client/react";
|
|
16
17
|
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
17
|
-
import { Button as h, GlassCard as g, Input as _, Label as v, PagePurpose as
|
|
18
|
+
import { Button as h, GlassCard as g, Input as _, Label as v, PagePurpose as Ce } from "@burdenoff/fe-libs/ui";
|
|
18
19
|
//#region src/assethandler/pages/FieldModePage.tsx
|
|
19
|
-
var
|
|
20
|
+
var we = f`
|
|
20
21
|
query AssetHandlerFieldAssets {
|
|
21
22
|
assets {
|
|
22
23
|
id
|
|
@@ -40,7 +41,7 @@ function y(e) {
|
|
|
40
41
|
status: e.status
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
|
-
var
|
|
44
|
+
var Te = f`
|
|
44
45
|
query AssetHandlerFieldAssetByTag($tagId: String!) {
|
|
45
46
|
assetByTag(tagId: $tagId) {
|
|
46
47
|
id
|
|
@@ -52,7 +53,7 @@ var we = f`
|
|
|
52
53
|
assignedTo
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
`,
|
|
56
|
+
`, Ee = f`
|
|
56
57
|
query AssetHandlerFieldLatestAssignment($assetId: ID!) {
|
|
57
58
|
assetAssignments(assetId: $assetId, pagination: { limit: 1 }) {
|
|
58
59
|
id
|
|
@@ -60,15 +61,15 @@ var we = f`
|
|
|
60
61
|
assignedTo
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
|
-
`, b = "assethandler.field.lastAssignee",
|
|
64
|
-
function
|
|
64
|
+
`, b = "assethandler.field.lastAssignee", De = 50;
|
|
65
|
+
function Oe() {
|
|
65
66
|
try {
|
|
66
67
|
return o()?.getItem(b) ?? "";
|
|
67
68
|
} catch {
|
|
68
69
|
return "";
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
|
-
function
|
|
72
|
+
function ke(e) {
|
|
72
73
|
try {
|
|
73
74
|
o()?.setItem(b, e);
|
|
74
75
|
} catch {}
|
|
@@ -78,7 +79,7 @@ function x() {
|
|
|
78
79
|
return /* @__PURE__ */ p(S, { workspaceId: e }, e || "default");
|
|
79
80
|
}
|
|
80
81
|
function S({ workspaceId: s }) {
|
|
81
|
-
let f = a(), b =
|
|
82
|
+
let f = a(), b = Se(), { currentUser: x } = t(), S = e((e) => e.refetchBootstrap), C = ae(s), [Ae] = d(() => ce(o(), C).assets), [je, w] = d(!1), [T, E] = d(""), [D, Me] = d(""), [O, Ne] = d(null), [k, A] = d(null), [j, Pe] = d(Oe), [M, N] = d(""), [Fe, P] = d(!1), F = u(null), I = u(null), L = u(0), [Ie, Le] = d(null), R = u(!1), z = u(0);
|
|
82
83
|
l(() => (R.current = !0, () => {
|
|
83
84
|
R.current = !1, z.current += 1;
|
|
84
85
|
}), []);
|
|
@@ -87,12 +88,12 @@ function S({ workspaceId: s }) {
|
|
|
87
88
|
z.current += 1;
|
|
88
89
|
let e = z.current;
|
|
89
90
|
b.query({
|
|
90
|
-
query:
|
|
91
|
+
query: we,
|
|
91
92
|
fetchPolicy: "network-only"
|
|
92
93
|
}).then((t) => {
|
|
93
94
|
if (!R.current || z.current !== e || !t.data?.assets) return;
|
|
94
95
|
let n = t.data.assets.map(y);
|
|
95
|
-
|
|
96
|
+
Le(n), ue(o(), C, n);
|
|
96
97
|
}, () => {});
|
|
97
98
|
}, [b, C]), V = c(async (e, t) => {
|
|
98
99
|
let n = {
|
|
@@ -102,7 +103,7 @@ function S({ workspaceId: s }) {
|
|
|
102
103
|
};
|
|
103
104
|
if (I.current = e.id, A(n), t) try {
|
|
104
105
|
let t = await b.query({
|
|
105
|
-
query:
|
|
106
|
+
query: Ee,
|
|
106
107
|
variables: { assetId: e.id },
|
|
107
108
|
fetchPolicy: "network-only"
|
|
108
109
|
});
|
|
@@ -114,7 +115,7 @@ function S({ workspaceId: s }) {
|
|
|
114
115
|
confirmed: !0
|
|
115
116
|
});
|
|
116
117
|
} catch {}
|
|
117
|
-
}, [b]), { queue: H, pending: U, online: W, syncing: G, sendingId: K, persisted:
|
|
118
|
+
}, [b]), { queue: H, pending: U, online: W, syncing: G, sendingId: K, persisted: Re, enqueue: ze, syncNow: Be, remove: Ve, retry: He } = ie({
|
|
118
119
|
workspaceId: s,
|
|
119
120
|
client: b,
|
|
120
121
|
onFlushed: (e) => {
|
|
@@ -126,8 +127,8 @@ function S({ workspaceId: s }) {
|
|
|
126
127
|
l(() => {
|
|
127
128
|
W && B();
|
|
128
129
|
}, [W, B]);
|
|
129
|
-
let q =
|
|
130
|
-
L.current += 1, P(!1),
|
|
130
|
+
let q = Ie ?? Ae, J = c((e) => {
|
|
131
|
+
L.current += 1, P(!1), Ne(e), N(""), V(e, W);
|
|
131
132
|
}, [V, W]), Y = c(async (e) => {
|
|
132
133
|
let t = le(e);
|
|
133
134
|
if (!t) {
|
|
@@ -148,7 +149,7 @@ function S({ workspaceId: s }) {
|
|
|
148
149
|
P(!0);
|
|
149
150
|
try {
|
|
150
151
|
let e = await b.query({
|
|
151
|
-
query:
|
|
152
|
+
query: Te,
|
|
152
153
|
variables: { tagId: t },
|
|
153
154
|
fetchPolicy: "network-only"
|
|
154
155
|
});
|
|
@@ -171,9 +172,13 @@ function S({ workspaceId: s }) {
|
|
|
171
172
|
W,
|
|
172
173
|
J,
|
|
173
174
|
f
|
|
174
|
-
]),
|
|
175
|
+
]), [Ue] = xe(), X = Ue.get("tag")?.trim() ?? "", We = u("");
|
|
176
|
+
l(() => {
|
|
177
|
+
!X || We.current === X || (We.current = X, Y(X));
|
|
178
|
+
}, [X, Y]);
|
|
179
|
+
let Ge = c((e) => {
|
|
175
180
|
w(!1), Y(e);
|
|
176
|
-
}, [Y]),
|
|
181
|
+
}, [Y]), Z = O ? ne(H, O.tagId, k?.checkedOut ?? !1) : !1, Ke = O ? re(H, O.tagId, k?.holder ?? "") : "", qe = (e) => {
|
|
177
182
|
if (!O) return;
|
|
178
183
|
let t = j.trim();
|
|
179
184
|
if (e === "CHECK_OUT" && !t) {
|
|
@@ -181,7 +186,7 @@ function S({ workspaceId: s }) {
|
|
|
181
186
|
return;
|
|
182
187
|
}
|
|
183
188
|
try {
|
|
184
|
-
|
|
189
|
+
ze({
|
|
185
190
|
kind: e,
|
|
186
191
|
tagId: O.tagId,
|
|
187
192
|
assetName: O.name,
|
|
@@ -195,8 +200,8 @@ function S({ workspaceId: s }) {
|
|
|
195
200
|
}
|
|
196
201
|
throw e;
|
|
197
202
|
}
|
|
198
|
-
e === "CHECK_OUT" &&
|
|
199
|
-
}, Q = de(() => oe(q, D).slice(0,
|
|
203
|
+
e === "CHECK_OUT" && ke(t), N(""), W || r.info(f("assethandler.field.savedOffline", "Saved on this device"), f("assethandler.field.savedOfflineHint", "It will sync automatically when you are back online.")), F.current?.focus();
|
|
204
|
+
}, Q = de(() => oe(q, D).slice(0, De), [q, D]), $ = (e) => e === "CHECK_OUT" ? f("assethandler.field.kind.checkOut", "Check out") : f("assethandler.field.kind.checkIn", "Check in");
|
|
200
205
|
return /* @__PURE__ */ m("div", {
|
|
201
206
|
className: "mx-auto max-w-2xl space-y-4 p-3 sm:p-6",
|
|
202
207
|
children: [
|
|
@@ -206,7 +211,7 @@ function S({ workspaceId: s }) {
|
|
|
206
211
|
title: f("assethandler.field.title", "Field Mode"),
|
|
207
212
|
description: f("assethandler.field.description", "Scan, check out and check in assets on site, with or without signal")
|
|
208
213
|
}),
|
|
209
|
-
/* @__PURE__ */ p(
|
|
214
|
+
/* @__PURE__ */ p(Ce, { children: f("assethandler.field.purpose", "Built for the phone in your hand. Scan a tag to open the asset, then check it out or in with one tap. Actions are saved on this device first and sync automatically, so a dead spot never loses a record.") }),
|
|
210
215
|
/* @__PURE__ */ m("div", {
|
|
211
216
|
role: "status",
|
|
212
217
|
"data-testid": "field-connectivity",
|
|
@@ -226,7 +231,7 @@ function S({ workspaceId: s }) {
|
|
|
226
231
|
type: "button",
|
|
227
232
|
size: "sm",
|
|
228
233
|
variant: "outline",
|
|
229
|
-
onClick: () => void
|
|
234
|
+
onClick: () => void Be(),
|
|
230
235
|
disabled: G,
|
|
231
236
|
children: [/* @__PURE__ */ p(ge, {
|
|
232
237
|
className: n("h-4 w-4", G && "animate-spin"),
|
|
@@ -235,7 +240,7 @@ function S({ workspaceId: s }) {
|
|
|
235
240
|
}) : null]
|
|
236
241
|
})]
|
|
237
242
|
}),
|
|
238
|
-
|
|
243
|
+
Re ? null : /* @__PURE__ */ p("p", {
|
|
239
244
|
role: "alert",
|
|
240
245
|
className: "text-sm text-status-error-text",
|
|
241
246
|
children: f("assethandler.field.notPersisted", "This browser is not letting the app save data, so queued actions will be lost if you close the page. Stay on this page until they sync.")
|
|
@@ -250,8 +255,8 @@ function S({ workspaceId: s }) {
|
|
|
250
255
|
"aria-hidden": "true"
|
|
251
256
|
}), f("assethandler.field.scanTitle", "Scan a tag")]
|
|
252
257
|
}),
|
|
253
|
-
|
|
254
|
-
onDetected:
|
|
258
|
+
je ? /* @__PURE__ */ p(ee, {
|
|
259
|
+
onDetected: Ge,
|
|
255
260
|
onClose: () => w(!1)
|
|
256
261
|
}) : /* @__PURE__ */ m(h, {
|
|
257
262
|
type: "button",
|
|
@@ -288,7 +293,7 @@ function S({ workspaceId: s }) {
|
|
|
288
293
|
type: "submit",
|
|
289
294
|
variant: "outline",
|
|
290
295
|
className: "h-12",
|
|
291
|
-
disabled:
|
|
296
|
+
disabled: Fe,
|
|
292
297
|
children: f("assethandler.field.find", "Find")
|
|
293
298
|
})
|
|
294
299
|
]
|
|
@@ -312,19 +317,19 @@ function S({ workspaceId: s }) {
|
|
|
312
317
|
})]
|
|
313
318
|
}), /* @__PURE__ */ p("span", {
|
|
314
319
|
"data-testid": "field-custody",
|
|
315
|
-
className: n("shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium",
|
|
316
|
-
children:
|
|
320
|
+
className: n("shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium", Z ? "bg-status-warning-subtle text-status-warning-text" : "bg-status-success-subtle text-status-success-text"),
|
|
321
|
+
children: Z ? f("assethandler.field.checkedOut", "Checked out") : f("assethandler.field.available", "Available")
|
|
317
322
|
})]
|
|
318
323
|
}),
|
|
319
|
-
|
|
324
|
+
Ke && Z ? /* @__PURE__ */ p("p", {
|
|
320
325
|
className: "text-sm text-text-secondary",
|
|
321
|
-
children: f("assethandler.field.heldBy", "Held by {{holder}}", { holder:
|
|
326
|
+
children: f("assethandler.field.heldBy", "Held by {{holder}}", { holder: Ke })
|
|
322
327
|
}) : null,
|
|
323
328
|
k && !k.confirmed ? /* @__PURE__ */ p("p", {
|
|
324
329
|
className: "text-xs text-text-secondary",
|
|
325
330
|
children: f("assethandler.field.unconfirmed", "Custody shown from this device’s last sync; the server confirms it when the action syncs.")
|
|
326
331
|
}) : null,
|
|
327
|
-
|
|
332
|
+
Z ? null : /* @__PURE__ */ m("div", {
|
|
328
333
|
className: "space-y-1",
|
|
329
334
|
children: [/* @__PURE__ */ p(v, {
|
|
330
335
|
htmlFor: "field-assignee",
|
|
@@ -332,7 +337,7 @@ function S({ workspaceId: s }) {
|
|
|
332
337
|
}), /* @__PURE__ */ p(_, {
|
|
333
338
|
id: "field-assignee",
|
|
334
339
|
value: j,
|
|
335
|
-
onChange: (e) =>
|
|
340
|
+
onChange: (e) => Pe(e.target.value),
|
|
336
341
|
placeholder: x?.email ?? "name@company.com",
|
|
337
342
|
autoComplete: "email",
|
|
338
343
|
className: "h-12"
|
|
@@ -356,8 +361,8 @@ function S({ workspaceId: s }) {
|
|
|
356
361
|
children: [/* @__PURE__ */ m(h, {
|
|
357
362
|
type: "button",
|
|
358
363
|
className: "h-14 text-base",
|
|
359
|
-
variant:
|
|
360
|
-
onClick: () =>
|
|
364
|
+
variant: Z ? "outline" : "default",
|
|
365
|
+
onClick: () => qe("CHECK_OUT"),
|
|
361
366
|
children: [/* @__PURE__ */ p(pe, {
|
|
362
367
|
className: "h-5 w-5",
|
|
363
368
|
"aria-hidden": "true"
|
|
@@ -365,8 +370,8 @@ function S({ workspaceId: s }) {
|
|
|
365
370
|
}), /* @__PURE__ */ m(h, {
|
|
366
371
|
type: "button",
|
|
367
372
|
className: "h-14 text-base",
|
|
368
|
-
variant:
|
|
369
|
-
onClick: () =>
|
|
373
|
+
variant: Z ? "default" : "outline",
|
|
374
|
+
onClick: () => qe("CHECK_IN"),
|
|
370
375
|
children: [/* @__PURE__ */ p(fe, {
|
|
371
376
|
className: "h-5 w-5",
|
|
372
377
|
"aria-hidden": "true"
|
|
@@ -404,13 +409,13 @@ function S({ workspaceId: s }) {
|
|
|
404
409
|
type: "button",
|
|
405
410
|
size: "sm",
|
|
406
411
|
variant: "outline",
|
|
407
|
-
onClick: () =>
|
|
412
|
+
onClick: () => He(e.id),
|
|
408
413
|
children: f("assethandler.field.retry", "Retry")
|
|
409
414
|
}) : null, /* @__PURE__ */ p(h, {
|
|
410
415
|
type: "button",
|
|
411
416
|
size: "icon",
|
|
412
417
|
variant: "ghost",
|
|
413
|
-
onClick: () =>
|
|
418
|
+
onClick: () => Ve(e.id),
|
|
414
419
|
disabled: e.id === K,
|
|
415
420
|
"aria-label": f("assethandler.field.discard", "Discard queued action"),
|
|
416
421
|
children: /* @__PURE__ */ p(ye, {
|
|
@@ -433,7 +438,7 @@ function S({ workspaceId: s }) {
|
|
|
433
438
|
/* @__PURE__ */ p(_, {
|
|
434
439
|
id: "field-search",
|
|
435
440
|
value: D,
|
|
436
|
-
onChange: (e) =>
|
|
441
|
+
onChange: (e) => Me(e.target.value),
|
|
437
442
|
placeholder: f("assethandler.field.searchPlaceholder", "Name, tag, location or holder"),
|
|
438
443
|
className: "h-12"
|
|
439
444
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FieldModePage.js","names":[],"sources":["../../../src/assethandler/pages/FieldModePage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient } from '@apollo/client/react';\nimport { Button, Input, Label, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n ArrowDownToLine,\n ArrowUpFromLine,\n Camera,\n CloudOff,\n RefreshCw,\n ScanLine,\n Smartphone,\n Trash2,\n Wifi,\n} from 'lucide-react';\nimport { SectionHeader } from '../components';\nimport { CameraTagScanner } from '../components/CameraTagScanner';\nimport { useAssetHandler } from '../providers/AssetHandlerProvider';\nimport { useAssetHandlerStore } from '../store/assethandlerStore';\nimport { browserStorage, useFieldQueue } from '../hooks/useFieldQueue';\nimport { useTr } from '../hooks/useTr';\nimport { notify } from '../utils/toast';\nimport { cn } from '../utils/cn';\nimport {\n FieldQueueFullError,\n projectedCustody,\n projectedHolder,\n type FieldAction,\n type FieldActionKind,\n} from '../utils/fieldQueue';\nimport {\n fieldAssetCacheKey,\n filterFieldAssets,\n findFieldAsset,\n loadFieldAssets,\n normalizeScannedTag,\n saveFieldAssets,\n toFieldAsset,\n type FieldAsset,\n} from '../utils/fieldAssetCache';\n\n/**\n * Field mode (BOFF-3952): a phone-first surface for technicians on site.\n * Scan a tag with the camera (or a handheld scanner / typed tag), see the\n * asset and who holds it, and check it out or in with one thumb. Actions are\n * queued on the device and replayed idempotently, so the page keeps working\n * without network and syncs when the connection returns.\n */\n\n/**\n * The registry the device keeps for offline lookups. Field mode fetches it\n * itself (network-only) rather than reading the shared bootstrap store: the\n * store can still hold the previous workspace's assets for a moment after a\n * workspace switch, and persisting those under the new workspace's cache key\n * would let an offline scan resolve against another workspace's registry.\n */\nexport const FIELD_ASSETS = gql`\n query AssetHandlerFieldAssets {\n assets {\n id\n name\n tagId\n assetNumber\n status\n locationName\n assignedTo\n }\n }\n`;\n\ninterface FieldAssetRow {\n id: string;\n name: string;\n tagId?: string | null;\n assetNumber?: string | null;\n status?: string | null;\n locationName?: string | null;\n assignedTo?: string | null;\n}\n\nfunction rowToFieldAsset(row: FieldAssetRow): FieldAsset {\n return toFieldAsset({\n id: row.id,\n name: row.name,\n tagId: row.tagId,\n assetTag: row.assetNumber,\n location: row.locationName,\n assignedTo: row.assignedTo,\n status: row.status,\n });\n}\n\nconst FIELD_ASSET_BY_TAG = gql`\n query AssetHandlerFieldAssetByTag($tagId: String!) {\n assetByTag(tagId: $tagId) {\n id\n name\n tagId\n assetNumber\n status\n locationName\n assignedTo\n }\n }\n`;\n\nconst FIELD_LATEST_ASSIGNMENT = gql`\n query AssetHandlerFieldLatestAssignment($assetId: ID!) {\n assetAssignments(assetId: $assetId, pagination: { limit: 1 }) {\n id\n status\n assignedTo\n }\n }\n`;\n\ninterface AssetByTagResult {\n assetByTag: {\n id: string;\n name: string;\n tagId: string;\n assetNumber: string;\n status: string;\n locationName: string | null;\n assignedTo: string | null;\n } | null;\n}\n\ninterface LatestAssignmentResult {\n assetAssignments: { id: string; status: string; assignedTo: string }[];\n}\n\n/** Custody as last confirmed by the server, or a hint when offline. */\ninterface CustodyView {\n checkedOut: boolean;\n holder: string;\n confirmed: boolean;\n}\n\nconst LAST_ASSIGNEE_KEY = 'assethandler.field.lastAssignee';\nconst LIST_LIMIT = 50;\n\nfunction readLastAssignee(): string {\n try {\n return browserStorage()?.getItem(LAST_ASSIGNEE_KEY) ?? '';\n } catch {\n return '';\n }\n}\n\nfunction rememberAssignee(value: string): void {\n try {\n browserStorage()?.setItem(LAST_ASSIGNEE_KEY, value);\n } catch {\n // Remembering the last recipient is a convenience only.\n }\n}\n\nexport function FieldModePage() {\n const { workspaceId } = useAssetHandler();\n // Remount per workspace so the queue and offline registry never leak\n // across a workspace switch.\n return <FieldModeView key={workspaceId || 'default'} workspaceId={workspaceId} />;\n}\n\nfunction FieldModeView({ workspaceId }: { workspaceId?: string | null }) {\n const tr = useTr();\n const apollo = useApolloClient();\n const { currentUser } = useAssetHandler();\n const refetchBootstrap = useAssetHandlerStore((state) => state.refetchBootstrap);\n\n const cacheKey = fieldAssetCacheKey(workspaceId);\n const [cachedAssets] = useState<FieldAsset[]>(\n () => loadFieldAssets(browserStorage(), cacheKey).assets\n );\n const [scannerOpen, setScannerOpen] = useState(false);\n const [tagInput, setTagInput] = useState('');\n const [search, setSearch] = useState('');\n const [selected, setSelected] = useState<FieldAsset | null>(null);\n const [custody, setCustody] = useState<CustodyView | null>(null);\n const [assignee, setAssignee] = useState(readLastAssignee);\n const [notes, setNotes] = useState('');\n const [lookingUp, setLookingUp] = useState(false);\n const tagInputRef = useRef<HTMLInputElement>(null);\n // The asset whose custody the card currently shows. A slower custody\n // response for an asset the technician has since moved away from must not\n // overwrite the newer selection.\n const custodyAssetIdRef = useRef<string | null>(null);\n // Bumped by every selection and every online tag lookup. A lookup whose\n // response arrives after the technician picked something else (from Browse\n // or a newer scan) is stale and must not replace that newer choice.\n const selectionSeqRef = useRef(0);\n\n const [liveAssets, setLiveAssets] = useState<FieldAsset[] | null>(null);\n // This view is keyed by workspace, so every response it receives was\n // requested for this workspace. `registryActiveRef` drops responses that\n // land after unmount (a workspace switch); it is re-armed on every effect\n // setup, so a StrictMode (or Activity) cleanup/setup cycle keeps loading.\n // Each cleanup also bumps the request counter, which invalidates requests\n // issued during an earlier effect lifetime, and the counter itself drops\n // out-of-order responses from an older refresh.\n const registryActiveRef = useRef(false);\n const registryRequestRef = useRef(0);\n useEffect(() => {\n registryActiveRef.current = true;\n return () => {\n registryActiveRef.current = false;\n registryRequestRef.current += 1;\n };\n }, []);\n\n const refreshRegistry = useCallback(() => {\n if (!registryActiveRef.current) return;\n registryRequestRef.current += 1;\n const request = registryRequestRef.current;\n apollo\n .query<{ assets: FieldAssetRow[] | null }>({\n query: FIELD_ASSETS,\n fetchPolicy: 'network-only',\n })\n .then(\n (result) => {\n if (\n !registryActiveRef.current ||\n registryRequestRef.current !== request ||\n !result.data?.assets\n ) {\n return;\n }\n const next = result.data.assets.map(rowToFieldAsset);\n setLiveAssets(next);\n // Keep the offline snapshot in step with the live registry.\n saveFieldAssets(browserStorage(), cacheKey, next);\n },\n () => {\n // Offline or a transient failure: keep using the device snapshot.\n }\n );\n }, [apollo, cacheKey]);\n\n const loadCustody = useCallback(\n async (asset: FieldAsset, online: boolean) => {\n const hint: CustodyView = {\n checkedOut: asset.assignedTo !== '',\n holder: asset.assignedTo,\n confirmed: false,\n };\n custodyAssetIdRef.current = asset.id;\n setCustody(hint);\n if (!online) return;\n try {\n const result = await apollo.query<LatestAssignmentResult>({\n query: FIELD_LATEST_ASSIGNMENT,\n variables: { assetId: asset.id },\n fetchPolicy: 'network-only',\n });\n if (custodyAssetIdRef.current !== asset.id) return;\n const latest = result.data?.assetAssignments?.[0];\n setCustody({\n checkedOut: !!latest && latest.status !== 'RETURNED',\n holder: latest && latest.status !== 'RETURNED' ? latest.assignedTo : '',\n confirmed: true,\n });\n } catch {\n // Keep the hint; the server still validates the action on sync.\n }\n },\n [apollo]\n );\n\n const queueApi = useFieldQueue({\n workspaceId,\n client: apollo,\n onFlushed: (result) => {\n if (result.synced.length > 0) {\n notify.success(\n tr('assethandler.field.synced', '{{count}} field action(s) synced', {\n count: result.synced.length,\n })\n );\n refetchBootstrap();\n refreshRegistry();\n }\n for (const action of result.rejected) {\n notify.error(\n tr('assethandler.field.rejected', 'Could not apply action for {{asset}}', {\n asset: action.assetName,\n }),\n action.lastError\n );\n }\n if (selected) void loadCustody(selected, true);\n },\n });\n const { queue, pending, online, syncing, sendingId, persisted, enqueue, syncNow, remove, retry } =\n queueApi;\n\n // Load the registry on open and again whenever connectivity returns.\n useEffect(() => {\n if (online) refreshRegistry();\n }, [online, refreshRegistry]);\n\n const knownAssets = liveAssets ?? cachedAssets;\n\n const selectAsset = useCallback(\n (asset: FieldAsset) => {\n selectionSeqRef.current += 1;\n setLookingUp(false);\n setSelected(asset);\n setNotes('');\n void loadCustody(asset, online);\n },\n [loadCustody, online]\n );\n\n const resolveTag = useCallback(\n async (raw: string) => {\n const tag = normalizeScannedTag(raw);\n if (!tag) {\n notify.warning(\n tr('assethandler.field.enterTag', 'Enter a tag'),\n tr('assethandler.field.enterTagHint', 'Scan or type the asset tag to continue.')\n );\n return;\n }\n const local = findFieldAsset(knownAssets, tag);\n if (local) {\n selectAsset(local);\n setTagInput('');\n return;\n }\n if (!online) {\n notify.warning(\n tr('assethandler.field.unknownOffline', 'Tag not in the offline registry'),\n tr(\n 'assethandler.field.unknownOfflineHint',\n 'Reconnect once so this device can refresh its copy of the registry.'\n )\n );\n return;\n }\n selectionSeqRef.current += 1;\n const lookup = selectionSeqRef.current;\n const isCurrent = () => selectionSeqRef.current === lookup;\n setLookingUp(true);\n try {\n const result = await apollo.query<AssetByTagResult>({\n query: FIELD_ASSET_BY_TAG,\n variables: { tagId: tag },\n fetchPolicy: 'network-only',\n });\n if (!isCurrent()) return;\n const found = result.data?.assetByTag;\n if (!found) {\n notify.error(\n tr('assethandler.field.notFound', 'No asset found'),\n tr('assethandler.field.notFoundHint', 'No asset matches tag \"{{tag}}\".', { tag })\n );\n return;\n }\n selectAsset(rowToFieldAsset(found));\n setTagInput('');\n } catch (error) {\n if (!isCurrent()) return;\n notify.error(\n tr('assethandler.field.lookupFailed', 'Tag lookup failed'),\n error instanceof Error ? error.message : undefined\n );\n } finally {\n if (isCurrent()) setLookingUp(false);\n }\n },\n [apollo, knownAssets, online, selectAsset, tr]\n );\n\n const onDetected = useCallback(\n (value: string) => {\n setScannerOpen(false);\n void resolveTag(value);\n },\n [resolveTag]\n );\n\n const projectedOut = selected\n ? projectedCustody(queue, selected.tagId, custody?.checkedOut ?? false)\n : false;\n const projectedHolderName = selected\n ? projectedHolder(queue, selected.tagId, custody?.holder ?? '')\n : '';\n\n const record = (kind: FieldActionKind) => {\n if (!selected) return;\n const recipient = assignee.trim();\n if (kind === 'CHECK_OUT' && !recipient) {\n notify.warning(\n tr('assethandler.field.assigneeRequired', 'Recipient required'),\n tr('assethandler.field.assigneeRequiredHint', 'Enter who is receiving the asset.')\n );\n return;\n }\n try {\n enqueue({\n kind,\n tagId: selected.tagId,\n assetName: selected.name,\n ...(kind === 'CHECK_OUT' ? { assignedTo: recipient } : {}),\n ...(notes.trim() ? { notes: notes.trim() } : {}),\n });\n } catch (error) {\n if (error instanceof FieldQueueFullError) {\n notify.error(\n tr('assethandler.field.queueFull', 'Offline queue is full'),\n tr(\n 'assethandler.field.queueFullHint',\n 'Reconnect to sync the queued actions before recording more.'\n )\n );\n return;\n }\n throw error;\n }\n if (kind === 'CHECK_OUT') rememberAssignee(recipient);\n setNotes('');\n if (!online) {\n notify.info(\n tr('assethandler.field.savedOffline', 'Saved on this device'),\n tr(\n 'assethandler.field.savedOfflineHint',\n 'It will sync automatically when you are back online.'\n )\n );\n }\n tagInputRef.current?.focus();\n };\n\n const visibleAssets = useMemo(\n () => filterFieldAssets(knownAssets, search).slice(0, LIST_LIMIT),\n [knownAssets, search]\n );\n\n const kindLabel = (kind: FieldActionKind) =>\n kind === 'CHECK_OUT'\n ? tr('assethandler.field.kind.checkOut', 'Check out')\n : tr('assethandler.field.kind.checkIn', 'Check in');\n\n return (\n <div className=\"mx-auto max-w-2xl space-y-4 p-3 sm:p-6\">\n <SectionHeader\n hero\n icon={Smartphone}\n title={tr('assethandler.field.title', 'Field Mode')}\n description={tr(\n 'assethandler.field.description',\n 'Scan, check out and check in assets on site, with or without signal'\n )}\n />\n <PagePurpose>\n {tr(\n 'assethandler.field.purpose',\n 'Built for the phone in your hand. Scan a tag to open the asset, then check it out or in with one tap. Actions are saved on this device first and sync automatically, so a dead spot never loses a record.'\n )}\n </PagePurpose>\n\n <div\n role=\"status\"\n data-testid=\"field-connectivity\"\n className={cn(\n 'flex flex-wrap items-center justify-between gap-2 rounded-xl px-3 py-2 text-sm',\n online\n ? 'bg-status-success-subtle text-status-success-text'\n : 'bg-status-warning-subtle text-status-warning-text'\n )}\n >\n <span className=\"flex items-center gap-2 font-medium\">\n {online ? (\n <Wifi className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <CloudOff className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {online\n ? tr('assethandler.field.online', 'Online')\n : tr('assethandler.field.offline', 'Offline — actions are saved on this device')}\n </span>\n <span className=\"flex items-center gap-2\">\n <span data-testid=\"field-pending-count\">\n {tr('assethandler.field.pending', '{{count}} waiting to sync', { count: pending })}\n </span>\n {pending > 0 && online ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={() => void syncNow()}\n disabled={syncing}\n >\n <RefreshCw className={cn('h-4 w-4', syncing && 'animate-spin')} aria-hidden=\"true\" />\n {tr('assethandler.field.syncNow', 'Sync now')}\n </Button>\n ) : null}\n </span>\n </div>\n {!persisted ? (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {tr(\n 'assethandler.field.notPersisted',\n 'This browser is not letting the app save data, so queued actions will be lost if you close the page. Stay on this page until they sync.'\n )}\n </p>\n ) : null}\n\n <GlassCard className=\"space-y-3 p-4\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <ScanLine className=\"h-4 w-4\" aria-hidden=\"true\" />\n {tr('assethandler.field.scanTitle', 'Scan a tag')}\n </div>\n {scannerOpen ? (\n <CameraTagScanner onDetected={onDetected} onClose={() => setScannerOpen(false)} />\n ) : (\n <Button\n type=\"button\"\n className=\"h-14 w-full text-base\"\n onClick={() => setScannerOpen(true)}\n >\n <Camera className=\"h-5 w-5\" aria-hidden=\"true\" />\n {tr('assethandler.field.openCamera', 'Scan with camera')}\n </Button>\n )}\n <form\n className=\"flex gap-2\"\n onSubmit={(event) => {\n event.preventDefault();\n void resolveTag(tagInput);\n }}\n >\n <Label htmlFor=\"field-tag-input\" className=\"sr-only\">\n {tr('assethandler.field.tagLabel', 'Asset tag')}\n </Label>\n <Input\n id=\"field-tag-input\"\n ref={tagInputRef}\n value={tagInput}\n onChange={(e) => setTagInput(e.target.value)}\n placeholder={tr('assethandler.field.tagPlaceholder', 'Type or scan a tag')}\n autoComplete=\"off\"\n autoCapitalize=\"off\"\n enterKeyHint=\"search\"\n className=\"h-12 flex-1\"\n />\n <Button type=\"submit\" variant=\"outline\" className=\"h-12\" disabled={lookingUp}>\n {tr('assethandler.field.find', 'Find')}\n </Button>\n </form>\n </GlassCard>\n\n {selected ? (\n <GlassCard className=\"space-y-4 p-4\" data-testid=\"field-selected-asset\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <h2 className=\"truncate text-lg font-semibold text-text-primary\">{selected.name}</h2>\n <p className=\"truncate text-xs text-text-secondary\">\n {tr('assethandler.field.tagValue', 'Tag {{tag}}', { tag: selected.tagId })}\n {selected.location ? ` · ${selected.location}` : ''}\n </p>\n </div>\n <span\n data-testid=\"field-custody\"\n className={cn(\n 'shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium',\n projectedOut\n ? 'bg-status-warning-subtle text-status-warning-text'\n : 'bg-status-success-subtle text-status-success-text'\n )}\n >\n {projectedOut\n ? tr('assethandler.field.checkedOut', 'Checked out')\n : tr('assethandler.field.available', 'Available')}\n </span>\n </div>\n {projectedHolderName && projectedOut ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('assethandler.field.heldBy', 'Held by {{holder}}', {\n holder: projectedHolderName,\n })}\n </p>\n ) : null}\n {custody && !custody.confirmed ? (\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'assethandler.field.unconfirmed',\n 'Custody shown from this device’s last sync; the server confirms it when the action syncs.'\n )}\n </p>\n ) : null}\n\n {!projectedOut ? (\n <div className=\"space-y-1\">\n <Label htmlFor=\"field-assignee\">\n {tr('assethandler.field.assignee', 'Checking out to')}\n </Label>\n <Input\n id=\"field-assignee\"\n value={assignee}\n onChange={(e) => setAssignee(e.target.value)}\n placeholder={currentUser?.email ?? 'name@company.com'}\n autoComplete=\"email\"\n className=\"h-12\"\n />\n </div>\n ) : null}\n <div className=\"space-y-1\">\n <Label htmlFor=\"field-notes\">\n {tr('assethandler.field.notes', 'Notes (optional)')}\n </Label>\n <Input\n id=\"field-notes\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={tr('assethandler.field.notesPlaceholder', 'Condition, location, job…')}\n className=\"h-12\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <Button\n type=\"button\"\n className=\"h-14 text-base\"\n variant={projectedOut ? 'outline' : 'default'}\n onClick={() => record('CHECK_OUT')}\n >\n <ArrowUpFromLine className=\"h-5 w-5\" aria-hidden=\"true\" />\n {kindLabel('CHECK_OUT')}\n </Button>\n <Button\n type=\"button\"\n className=\"h-14 text-base\"\n variant={projectedOut ? 'default' : 'outline'}\n onClick={() => record('CHECK_IN')}\n >\n <ArrowDownToLine className=\"h-5 w-5\" aria-hidden=\"true\" />\n {kindLabel('CHECK_IN')}\n </Button>\n </div>\n </GlassCard>\n ) : null}\n\n {queue.length > 0 ? (\n <GlassCard className=\"space-y-2 p-4\" data-testid=\"field-queue\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n {tr('assethandler.field.queueTitle', 'On this device')}\n </h2>\n <ul className=\"divide-y divide-border-seam\">\n {queue.map((action: FieldAction) => (\n <li key={action.id} className=\"flex items-center justify-between gap-3 py-2\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-primary\">\n {kindLabel(action.kind)} · {action.assetName}\n </p>\n <p\n className={cn(\n 'truncate text-xs',\n action.state === 'rejected' ? 'text-status-error-text' : 'text-text-secondary'\n )}\n >\n {action.state === 'rejected'\n ? (action.lastError ??\n tr('assethandler.field.refused', 'Refused by the server'))\n : action.id === sendingId\n ? tr('assethandler.field.sending', 'Sending…')\n : new Date(action.scannedAt).toLocaleString()}\n </p>\n </div>\n <div className=\"flex shrink-0 gap-1\">\n {action.state === 'rejected' ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={() => retry(action.id)}\n >\n {tr('assethandler.field.retry', 'Retry')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"ghost\"\n onClick={() => remove(action.id)}\n disabled={action.id === sendingId}\n aria-label={tr('assethandler.field.discard', 'Discard queued action')}\n >\n <Trash2 className=\"h-4 w-4\" aria-hidden=\"true\" />\n </Button>\n </div>\n </li>\n ))}\n </ul>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"space-y-3 p-4\">\n <Label htmlFor=\"field-search\" className=\"text-sm font-semibold\">\n {tr('assethandler.field.browse', 'Browse assets')}\n </Label>\n <Input\n id=\"field-search\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder={tr('assethandler.field.searchPlaceholder', 'Name, tag, location or holder')}\n className=\"h-12\"\n />\n {visibleAssets.length === 0 ? (\n <p className=\"py-4 text-center text-sm text-text-secondary\">\n {knownAssets.length === 0\n ? tr(\n 'assethandler.field.noAssets',\n 'No assets on this device yet. Open this page once while online to load the registry.'\n )\n : tr('assethandler.field.noMatches', 'No assets match your search.')}\n </p>\n ) : (\n <ul className=\"space-y-2\" data-testid=\"field-asset-list\">\n {visibleAssets.map((asset) => (\n <li key={asset.id}>\n <button\n type=\"button\"\n onClick={() => selectAsset(asset)}\n className={cn(\n 'flex min-h-14 w-full items-center justify-between gap-3 rounded-xl border border-border-seam px-3 py-2 text-left transition-colors hover:bg-bg-sunken',\n selected?.id === asset.id && 'border-action-primary-bg'\n )}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {asset.name}\n </span>\n <span className=\"block truncate text-xs text-text-secondary\">\n {asset.assetTag || asset.tagId}\n {asset.location ? ` · ${asset.location}` : ''}\n </span>\n </span>\n {asset.assignedTo ? (\n <span className=\"max-w-[40%] shrink-0 truncate rounded-full bg-status-warning-subtle px-2 py-0.5 text-[11px] text-status-warning-text\">\n {asset.assignedTo}\n </span>\n ) : null}\n </button>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FieldModePage.js","names":[],"sources":["../../../src/assethandler/pages/FieldModePage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient } from '@apollo/client/react';\nimport { useSearchParams } from 'react-router';\nimport { Button, Input, Label, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n ArrowDownToLine,\n ArrowUpFromLine,\n Camera,\n CloudOff,\n RefreshCw,\n ScanLine,\n Smartphone,\n Trash2,\n Wifi,\n} from 'lucide-react';\nimport { SectionHeader } from '../components';\nimport { CameraTagScanner } from '../components/CameraTagScanner';\nimport { useAssetHandler } from '../providers/AssetHandlerProvider';\nimport { useAssetHandlerStore } from '../store/assethandlerStore';\nimport { browserStorage, useFieldQueue } from '../hooks/useFieldQueue';\nimport { useTr } from '../hooks/useTr';\nimport { notify } from '../utils/toast';\nimport { cn } from '../utils/cn';\nimport {\n FieldQueueFullError,\n projectedCustody,\n projectedHolder,\n type FieldAction,\n type FieldActionKind,\n} from '../utils/fieldQueue';\nimport {\n fieldAssetCacheKey,\n filterFieldAssets,\n findFieldAsset,\n loadFieldAssets,\n normalizeScannedTag,\n saveFieldAssets,\n toFieldAsset,\n type FieldAsset,\n} from '../utils/fieldAssetCache';\n\n/**\n * Field mode (BOFF-3952): a phone-first surface for technicians on site.\n * Scan a tag with the camera (or a handheld scanner / typed tag), see the\n * asset and who holds it, and check it out or in with one thumb. Actions are\n * queued on the device and replayed idempotently, so the page keeps working\n * without network and syncs when the connection returns.\n */\n\n/**\n * The registry the device keeps for offline lookups. Field mode fetches it\n * itself (network-only) rather than reading the shared bootstrap store: the\n * store can still hold the previous workspace's assets for a moment after a\n * workspace switch, and persisting those under the new workspace's cache key\n * would let an offline scan resolve against another workspace's registry.\n */\nexport const FIELD_ASSETS = gql`\n query AssetHandlerFieldAssets {\n assets {\n id\n name\n tagId\n assetNumber\n status\n locationName\n assignedTo\n }\n }\n`;\n\ninterface FieldAssetRow {\n id: string;\n name: string;\n tagId?: string | null;\n assetNumber?: string | null;\n status?: string | null;\n locationName?: string | null;\n assignedTo?: string | null;\n}\n\nfunction rowToFieldAsset(row: FieldAssetRow): FieldAsset {\n return toFieldAsset({\n id: row.id,\n name: row.name,\n tagId: row.tagId,\n assetTag: row.assetNumber,\n location: row.locationName,\n assignedTo: row.assignedTo,\n status: row.status,\n });\n}\n\n/** Online lookup of one asset by its printed tag (scan or deep link). */\nexport const FIELD_ASSET_BY_TAG = gql`\n query AssetHandlerFieldAssetByTag($tagId: String!) {\n assetByTag(tagId: $tagId) {\n id\n name\n tagId\n assetNumber\n status\n locationName\n assignedTo\n }\n }\n`;\n\nconst FIELD_LATEST_ASSIGNMENT = gql`\n query AssetHandlerFieldLatestAssignment($assetId: ID!) {\n assetAssignments(assetId: $assetId, pagination: { limit: 1 }) {\n id\n status\n assignedTo\n }\n }\n`;\n\ninterface AssetByTagResult {\n assetByTag: {\n id: string;\n name: string;\n tagId: string;\n assetNumber: string;\n status: string;\n locationName: string | null;\n assignedTo: string | null;\n } | null;\n}\n\ninterface LatestAssignmentResult {\n assetAssignments: { id: string; status: string; assignedTo: string }[];\n}\n\n/** Custody as last confirmed by the server, or a hint when offline. */\ninterface CustodyView {\n checkedOut: boolean;\n holder: string;\n confirmed: boolean;\n}\n\nconst LAST_ASSIGNEE_KEY = 'assethandler.field.lastAssignee';\nconst LIST_LIMIT = 50;\n\nfunction readLastAssignee(): string {\n try {\n return browserStorage()?.getItem(LAST_ASSIGNEE_KEY) ?? '';\n } catch {\n return '';\n }\n}\n\nfunction rememberAssignee(value: string): void {\n try {\n browserStorage()?.setItem(LAST_ASSIGNEE_KEY, value);\n } catch {\n // Remembering the last recipient is a convenience only.\n }\n}\n\nexport function FieldModePage() {\n const { workspaceId } = useAssetHandler();\n // Remount per workspace so the queue and offline registry never leak\n // across a workspace switch.\n return <FieldModeView key={workspaceId || 'default'} workspaceId={workspaceId} />;\n}\n\nfunction FieldModeView({ workspaceId }: { workspaceId?: string | null }) {\n const tr = useTr();\n const apollo = useApolloClient();\n const { currentUser } = useAssetHandler();\n const refetchBootstrap = useAssetHandlerStore((state) => state.refetchBootstrap);\n\n const cacheKey = fieldAssetCacheKey(workspaceId);\n const [cachedAssets] = useState<FieldAsset[]>(\n () => loadFieldAssets(browserStorage(), cacheKey).assets\n );\n const [scannerOpen, setScannerOpen] = useState(false);\n const [tagInput, setTagInput] = useState('');\n const [search, setSearch] = useState('');\n const [selected, setSelected] = useState<FieldAsset | null>(null);\n const [custody, setCustody] = useState<CustodyView | null>(null);\n const [assignee, setAssignee] = useState(readLastAssignee);\n const [notes, setNotes] = useState('');\n const [lookingUp, setLookingUp] = useState(false);\n const tagInputRef = useRef<HTMLInputElement>(null);\n // The asset whose custody the card currently shows. A slower custody\n // response for an asset the technician has since moved away from must not\n // overwrite the newer selection.\n const custodyAssetIdRef = useRef<string | null>(null);\n // Bumped by every selection and every online tag lookup. A lookup whose\n // response arrives after the technician picked something else (from Browse\n // or a newer scan) is stale and must not replace that newer choice.\n const selectionSeqRef = useRef(0);\n\n const [liveAssets, setLiveAssets] = useState<FieldAsset[] | null>(null);\n // This view is keyed by workspace, so every response it receives was\n // requested for this workspace. `registryActiveRef` drops responses that\n // land after unmount (a workspace switch); it is re-armed on every effect\n // setup, so a StrictMode (or Activity) cleanup/setup cycle keeps loading.\n // Each cleanup also bumps the request counter, which invalidates requests\n // issued during an earlier effect lifetime, and the counter itself drops\n // out-of-order responses from an older refresh.\n const registryActiveRef = useRef(false);\n const registryRequestRef = useRef(0);\n useEffect(() => {\n registryActiveRef.current = true;\n return () => {\n registryActiveRef.current = false;\n registryRequestRef.current += 1;\n };\n }, []);\n\n const refreshRegistry = useCallback(() => {\n if (!registryActiveRef.current) return;\n registryRequestRef.current += 1;\n const request = registryRequestRef.current;\n apollo\n .query<{ assets: FieldAssetRow[] | null }>({\n query: FIELD_ASSETS,\n fetchPolicy: 'network-only',\n })\n .then(\n (result) => {\n if (\n !registryActiveRef.current ||\n registryRequestRef.current !== request ||\n !result.data?.assets\n ) {\n return;\n }\n const next = result.data.assets.map(rowToFieldAsset);\n setLiveAssets(next);\n // Keep the offline snapshot in step with the live registry.\n saveFieldAssets(browserStorage(), cacheKey, next);\n },\n () => {\n // Offline or a transient failure: keep using the device snapshot.\n }\n );\n }, [apollo, cacheKey]);\n\n const loadCustody = useCallback(\n async (asset: FieldAsset, online: boolean) => {\n const hint: CustodyView = {\n checkedOut: asset.assignedTo !== '',\n holder: asset.assignedTo,\n confirmed: false,\n };\n custodyAssetIdRef.current = asset.id;\n setCustody(hint);\n if (!online) return;\n try {\n const result = await apollo.query<LatestAssignmentResult>({\n query: FIELD_LATEST_ASSIGNMENT,\n variables: { assetId: asset.id },\n fetchPolicy: 'network-only',\n });\n if (custodyAssetIdRef.current !== asset.id) return;\n const latest = result.data?.assetAssignments?.[0];\n setCustody({\n checkedOut: !!latest && latest.status !== 'RETURNED',\n holder: latest && latest.status !== 'RETURNED' ? latest.assignedTo : '',\n confirmed: true,\n });\n } catch {\n // Keep the hint; the server still validates the action on sync.\n }\n },\n [apollo]\n );\n\n const queueApi = useFieldQueue({\n workspaceId,\n client: apollo,\n onFlushed: (result) => {\n if (result.synced.length > 0) {\n notify.success(\n tr('assethandler.field.synced', '{{count}} field action(s) synced', {\n count: result.synced.length,\n })\n );\n refetchBootstrap();\n refreshRegistry();\n }\n for (const action of result.rejected) {\n notify.error(\n tr('assethandler.field.rejected', 'Could not apply action for {{asset}}', {\n asset: action.assetName,\n }),\n action.lastError\n );\n }\n if (selected) void loadCustody(selected, true);\n },\n });\n const { queue, pending, online, syncing, sendingId, persisted, enqueue, syncNow, remove, retry } =\n queueApi;\n\n // Load the registry on open and again whenever connectivity returns.\n useEffect(() => {\n if (online) refreshRegistry();\n }, [online, refreshRegistry]);\n\n const knownAssets = liveAssets ?? cachedAssets;\n\n const selectAsset = useCallback(\n (asset: FieldAsset) => {\n selectionSeqRef.current += 1;\n setLookingUp(false);\n setSelected(asset);\n setNotes('');\n void loadCustody(asset, online);\n },\n [loadCustody, online]\n );\n\n const resolveTag = useCallback(\n async (raw: string) => {\n const tag = normalizeScannedTag(raw);\n if (!tag) {\n notify.warning(\n tr('assethandler.field.enterTag', 'Enter a tag'),\n tr('assethandler.field.enterTagHint', 'Scan or type the asset tag to continue.')\n );\n return;\n }\n const local = findFieldAsset(knownAssets, tag);\n if (local) {\n selectAsset(local);\n setTagInput('');\n return;\n }\n if (!online) {\n notify.warning(\n tr('assethandler.field.unknownOffline', 'Tag not in the offline registry'),\n tr(\n 'assethandler.field.unknownOfflineHint',\n 'Reconnect once so this device can refresh its copy of the registry.'\n )\n );\n return;\n }\n selectionSeqRef.current += 1;\n const lookup = selectionSeqRef.current;\n const isCurrent = () => selectionSeqRef.current === lookup;\n setLookingUp(true);\n try {\n const result = await apollo.query<AssetByTagResult>({\n query: FIELD_ASSET_BY_TAG,\n variables: { tagId: tag },\n fetchPolicy: 'network-only',\n });\n if (!isCurrent()) return;\n const found = result.data?.assetByTag;\n if (!found) {\n notify.error(\n tr('assethandler.field.notFound', 'No asset found'),\n tr('assethandler.field.notFoundHint', 'No asset matches tag \"{{tag}}\".', { tag })\n );\n return;\n }\n selectAsset(rowToFieldAsset(found));\n setTagInput('');\n } catch (error) {\n if (!isCurrent()) return;\n notify.error(\n tr('assethandler.field.lookupFailed', 'Tag lookup failed'),\n error instanceof Error ? error.message : undefined\n );\n } finally {\n if (isCurrent()) setLookingUp(false);\n }\n },\n [apollo, knownAssets, online, selectAsset, tr]\n );\n\n // Notifications deep-link here as `/assethandler/field?tag=<tagId>` (a\n // maintenance-due push for an asset the technician holds, BOFF-10631).\n // Open that asset once per linked tag, through the same lookup a scan uses,\n // so an offline device still resolves it from its registry copy.\n const [searchParams] = useSearchParams();\n const linkedTag = searchParams.get('tag')?.trim() ?? '';\n const openedLinkRef = useRef('');\n useEffect(() => {\n if (!linkedTag || openedLinkRef.current === linkedTag) return;\n openedLinkRef.current = linkedTag;\n void resolveTag(linkedTag);\n }, [linkedTag, resolveTag]);\n\n const onDetected = useCallback(\n (value: string) => {\n setScannerOpen(false);\n void resolveTag(value);\n },\n [resolveTag]\n );\n\n const projectedOut = selected\n ? projectedCustody(queue, selected.tagId, custody?.checkedOut ?? false)\n : false;\n const projectedHolderName = selected\n ? projectedHolder(queue, selected.tagId, custody?.holder ?? '')\n : '';\n\n const record = (kind: FieldActionKind) => {\n if (!selected) return;\n const recipient = assignee.trim();\n if (kind === 'CHECK_OUT' && !recipient) {\n notify.warning(\n tr('assethandler.field.assigneeRequired', 'Recipient required'),\n tr('assethandler.field.assigneeRequiredHint', 'Enter who is receiving the asset.')\n );\n return;\n }\n try {\n enqueue({\n kind,\n tagId: selected.tagId,\n assetName: selected.name,\n ...(kind === 'CHECK_OUT' ? { assignedTo: recipient } : {}),\n ...(notes.trim() ? { notes: notes.trim() } : {}),\n });\n } catch (error) {\n if (error instanceof FieldQueueFullError) {\n notify.error(\n tr('assethandler.field.queueFull', 'Offline queue is full'),\n tr(\n 'assethandler.field.queueFullHint',\n 'Reconnect to sync the queued actions before recording more.'\n )\n );\n return;\n }\n throw error;\n }\n if (kind === 'CHECK_OUT') rememberAssignee(recipient);\n setNotes('');\n if (!online) {\n notify.info(\n tr('assethandler.field.savedOffline', 'Saved on this device'),\n tr(\n 'assethandler.field.savedOfflineHint',\n 'It will sync automatically when you are back online.'\n )\n );\n }\n tagInputRef.current?.focus();\n };\n\n const visibleAssets = useMemo(\n () => filterFieldAssets(knownAssets, search).slice(0, LIST_LIMIT),\n [knownAssets, search]\n );\n\n const kindLabel = (kind: FieldActionKind) =>\n kind === 'CHECK_OUT'\n ? tr('assethandler.field.kind.checkOut', 'Check out')\n : tr('assethandler.field.kind.checkIn', 'Check in');\n\n return (\n <div className=\"mx-auto max-w-2xl space-y-4 p-3 sm:p-6\">\n <SectionHeader\n hero\n icon={Smartphone}\n title={tr('assethandler.field.title', 'Field Mode')}\n description={tr(\n 'assethandler.field.description',\n 'Scan, check out and check in assets on site, with or without signal'\n )}\n />\n <PagePurpose>\n {tr(\n 'assethandler.field.purpose',\n 'Built for the phone in your hand. Scan a tag to open the asset, then check it out or in with one tap. Actions are saved on this device first and sync automatically, so a dead spot never loses a record.'\n )}\n </PagePurpose>\n\n <div\n role=\"status\"\n data-testid=\"field-connectivity\"\n className={cn(\n 'flex flex-wrap items-center justify-between gap-2 rounded-xl px-3 py-2 text-sm',\n online\n ? 'bg-status-success-subtle text-status-success-text'\n : 'bg-status-warning-subtle text-status-warning-text'\n )}\n >\n <span className=\"flex items-center gap-2 font-medium\">\n {online ? (\n <Wifi className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <CloudOff className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {online\n ? tr('assethandler.field.online', 'Online')\n : tr('assethandler.field.offline', 'Offline — actions are saved on this device')}\n </span>\n <span className=\"flex items-center gap-2\">\n <span data-testid=\"field-pending-count\">\n {tr('assethandler.field.pending', '{{count}} waiting to sync', { count: pending })}\n </span>\n {pending > 0 && online ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={() => void syncNow()}\n disabled={syncing}\n >\n <RefreshCw className={cn('h-4 w-4', syncing && 'animate-spin')} aria-hidden=\"true\" />\n {tr('assethandler.field.syncNow', 'Sync now')}\n </Button>\n ) : null}\n </span>\n </div>\n {!persisted ? (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {tr(\n 'assethandler.field.notPersisted',\n 'This browser is not letting the app save data, so queued actions will be lost if you close the page. Stay on this page until they sync.'\n )}\n </p>\n ) : null}\n\n <GlassCard className=\"space-y-3 p-4\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <ScanLine className=\"h-4 w-4\" aria-hidden=\"true\" />\n {tr('assethandler.field.scanTitle', 'Scan a tag')}\n </div>\n {scannerOpen ? (\n <CameraTagScanner onDetected={onDetected} onClose={() => setScannerOpen(false)} />\n ) : (\n <Button\n type=\"button\"\n className=\"h-14 w-full text-base\"\n onClick={() => setScannerOpen(true)}\n >\n <Camera className=\"h-5 w-5\" aria-hidden=\"true\" />\n {tr('assethandler.field.openCamera', 'Scan with camera')}\n </Button>\n )}\n <form\n className=\"flex gap-2\"\n onSubmit={(event) => {\n event.preventDefault();\n void resolveTag(tagInput);\n }}\n >\n <Label htmlFor=\"field-tag-input\" className=\"sr-only\">\n {tr('assethandler.field.tagLabel', 'Asset tag')}\n </Label>\n <Input\n id=\"field-tag-input\"\n ref={tagInputRef}\n value={tagInput}\n onChange={(e) => setTagInput(e.target.value)}\n placeholder={tr('assethandler.field.tagPlaceholder', 'Type or scan a tag')}\n autoComplete=\"off\"\n autoCapitalize=\"off\"\n enterKeyHint=\"search\"\n className=\"h-12 flex-1\"\n />\n <Button type=\"submit\" variant=\"outline\" className=\"h-12\" disabled={lookingUp}>\n {tr('assethandler.field.find', 'Find')}\n </Button>\n </form>\n </GlassCard>\n\n {selected ? (\n <GlassCard className=\"space-y-4 p-4\" data-testid=\"field-selected-asset\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <h2 className=\"truncate text-lg font-semibold text-text-primary\">{selected.name}</h2>\n <p className=\"truncate text-xs text-text-secondary\">\n {tr('assethandler.field.tagValue', 'Tag {{tag}}', { tag: selected.tagId })}\n {selected.location ? ` · ${selected.location}` : ''}\n </p>\n </div>\n <span\n data-testid=\"field-custody\"\n className={cn(\n 'shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium',\n projectedOut\n ? 'bg-status-warning-subtle text-status-warning-text'\n : 'bg-status-success-subtle text-status-success-text'\n )}\n >\n {projectedOut\n ? tr('assethandler.field.checkedOut', 'Checked out')\n : tr('assethandler.field.available', 'Available')}\n </span>\n </div>\n {projectedHolderName && projectedOut ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('assethandler.field.heldBy', 'Held by {{holder}}', {\n holder: projectedHolderName,\n })}\n </p>\n ) : null}\n {custody && !custody.confirmed ? (\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'assethandler.field.unconfirmed',\n 'Custody shown from this device’s last sync; the server confirms it when the action syncs.'\n )}\n </p>\n ) : null}\n\n {!projectedOut ? (\n <div className=\"space-y-1\">\n <Label htmlFor=\"field-assignee\">\n {tr('assethandler.field.assignee', 'Checking out to')}\n </Label>\n <Input\n id=\"field-assignee\"\n value={assignee}\n onChange={(e) => setAssignee(e.target.value)}\n placeholder={currentUser?.email ?? 'name@company.com'}\n autoComplete=\"email\"\n className=\"h-12\"\n />\n </div>\n ) : null}\n <div className=\"space-y-1\">\n <Label htmlFor=\"field-notes\">\n {tr('assethandler.field.notes', 'Notes (optional)')}\n </Label>\n <Input\n id=\"field-notes\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={tr('assethandler.field.notesPlaceholder', 'Condition, location, job…')}\n className=\"h-12\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <Button\n type=\"button\"\n className=\"h-14 text-base\"\n variant={projectedOut ? 'outline' : 'default'}\n onClick={() => record('CHECK_OUT')}\n >\n <ArrowUpFromLine className=\"h-5 w-5\" aria-hidden=\"true\" />\n {kindLabel('CHECK_OUT')}\n </Button>\n <Button\n type=\"button\"\n className=\"h-14 text-base\"\n variant={projectedOut ? 'default' : 'outline'}\n onClick={() => record('CHECK_IN')}\n >\n <ArrowDownToLine className=\"h-5 w-5\" aria-hidden=\"true\" />\n {kindLabel('CHECK_IN')}\n </Button>\n </div>\n </GlassCard>\n ) : null}\n\n {queue.length > 0 ? (\n <GlassCard className=\"space-y-2 p-4\" data-testid=\"field-queue\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n {tr('assethandler.field.queueTitle', 'On this device')}\n </h2>\n <ul className=\"divide-y divide-border-seam\">\n {queue.map((action: FieldAction) => (\n <li key={action.id} className=\"flex items-center justify-between gap-3 py-2\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-primary\">\n {kindLabel(action.kind)} · {action.assetName}\n </p>\n <p\n className={cn(\n 'truncate text-xs',\n action.state === 'rejected' ? 'text-status-error-text' : 'text-text-secondary'\n )}\n >\n {action.state === 'rejected'\n ? (action.lastError ??\n tr('assethandler.field.refused', 'Refused by the server'))\n : action.id === sendingId\n ? tr('assethandler.field.sending', 'Sending…')\n : new Date(action.scannedAt).toLocaleString()}\n </p>\n </div>\n <div className=\"flex shrink-0 gap-1\">\n {action.state === 'rejected' ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={() => retry(action.id)}\n >\n {tr('assethandler.field.retry', 'Retry')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"ghost\"\n onClick={() => remove(action.id)}\n disabled={action.id === sendingId}\n aria-label={tr('assethandler.field.discard', 'Discard queued action')}\n >\n <Trash2 className=\"h-4 w-4\" aria-hidden=\"true\" />\n </Button>\n </div>\n </li>\n ))}\n </ul>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"space-y-3 p-4\">\n <Label htmlFor=\"field-search\" className=\"text-sm font-semibold\">\n {tr('assethandler.field.browse', 'Browse assets')}\n </Label>\n <Input\n id=\"field-search\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder={tr('assethandler.field.searchPlaceholder', 'Name, tag, location or holder')}\n className=\"h-12\"\n />\n {visibleAssets.length === 0 ? (\n <p className=\"py-4 text-center text-sm text-text-secondary\">\n {knownAssets.length === 0\n ? tr(\n 'assethandler.field.noAssets',\n 'No assets on this device yet. Open this page once while online to load the registry.'\n )\n : tr('assethandler.field.noMatches', 'No assets match your search.')}\n </p>\n ) : (\n <ul className=\"space-y-2\" data-testid=\"field-asset-list\">\n {visibleAssets.map((asset) => (\n <li key={asset.id}>\n <button\n type=\"button\"\n onClick={() => selectAsset(asset)}\n className={cn(\n 'flex min-h-14 w-full items-center justify-between gap-3 rounded-xl border border-border-seam px-3 py-2 text-left transition-colors hover:bg-bg-sunken',\n selected?.id === asset.id && 'border-action-primary-bg'\n )}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {asset.name}\n </span>\n <span className=\"block truncate text-xs text-text-secondary\">\n {asset.assetTag || asset.tagId}\n {asset.location ? ` · ${asset.location}` : ''}\n </span>\n </span>\n {asset.assignedTo ? (\n <span className=\"max-w-[40%] shrink-0 truncate rounded-full bg-status-warning-subtle px-2 py-0.5 text-[11px] text-status-warning-text\">\n {asset.assignedTo}\n </span>\n ) : null}\n </button>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </div>\n );\n}\n"],"mappings":"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"}
|