@burdenoff/microfe-assethandler 2026.1103.3 → 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.
|
@@ -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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"}
|