@burdenoff/microfe-assethandler 2026.1103.3 → 2026.1107.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ import { FieldWorkOrder } from '../utils/fieldWorkOrders';
2
+ import { FieldChecklistChange } from '../utils/fieldQueue';
3
+
4
+ export interface FieldWorkOrderCompletion {
5
+ notes?: string;
6
+ actualCost?: number;
7
+ checklist: FieldChecklistChange[];
8
+ }
9
+ export interface FieldWorkOrderItemProps {
10
+ workOrder: FieldWorkOrder;
11
+ /** Status once every queued action on this work order has been applied. */
12
+ projectedStatus: string;
13
+ /** True while a queued action for this work order waits to sync. */
14
+ queued: boolean;
15
+ /** Display name of the linked asset, when known on this device. */
16
+ assetName?: string;
17
+ onStart: (workOrder: FieldWorkOrder) => void;
18
+ /** Returns false when the completion could not be queued (form stays open). */
19
+ onComplete: (workOrder: FieldWorkOrder, completion: FieldWorkOrderCompletion) => boolean;
20
+ }
21
+ /**
22
+ * One work order in Field Mode (BOFF-10632): its status as the device projects
23
+ * it, a one-tap "Start work", and an inline completion form (notes, actual
24
+ * cost, checklist). Both actions are queued on the device by the caller.
25
+ */
26
+ export declare function FieldWorkOrderItem({ workOrder, projectedStatus, queued, assetName, onStart, onComplete, }: FieldWorkOrderItemProps): import("react").JSX.Element;
27
+ //# sourceMappingURL=FieldWorkOrderItem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldWorkOrderItem.d.ts","sourceRoot":"","sources":["../../../src/assethandler/components/FieldWorkOrderItem.tsx"],"names":[],"mappings":"AAKA,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,oBAAoB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,cAAc,CAAC;IAC1B,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,MAAM,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,CAAC,SAAS,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,+EAA+E;IAC/E,UAAU,EAAE,CAAC,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,wBAAwB,KAAK,OAAO,CAAC;CAC1F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,eAAe,EACf,MAAM,EACN,SAAS,EACT,OAAO,EACP,UAAU,GACX,EAAE,uBAAuB,+BAmMzB"}
@@ -0,0 +1,168 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { useTr as t } from "../hooks/useTr.js";
3
+ import { checklistChanges as n, isOpenWorkOrderStatus as r, parseActualCost as i } from "../utils/fieldWorkOrders.js";
4
+ import { useState as a } from "react";
5
+ import { CheckCircle as o, Play as s, Wrench as c } from "lucide-react";
6
+ import { jsx as l, jsxs as u } from "react/jsx-runtime";
7
+ import { Button as d, Input as f, Label as p } from "@burdenoff/fe-libs/ui";
8
+ //#region src/assethandler/components/FieldWorkOrderItem.tsx
9
+ function m({ workOrder: m, projectedStatus: h, queued: g, assetName: _, onStart: v, onComplete: y }) {
10
+ let b = t(), [x, S] = a(!1), [C, w] = a(""), [T, E] = a(""), [D, O] = a(!1), [k, A] = a(() => m.checklist.map((e) => e.completed)), j = r(h), M = h === "IN_PROGRESS", N = `field-work-order-${m.id}`, P = (e) => {
11
+ switch (e) {
12
+ case "DRAFT": return b("assethandler.field.wo.status.draft", "Draft");
13
+ case "APPROVED": return b("assethandler.field.wo.status.approved", "Approved");
14
+ case "SCHEDULED": return b("assethandler.field.wo.status.scheduled", "Scheduled");
15
+ case "IN_PROGRESS": return b("assethandler.field.wo.status.inProgress", "In progress");
16
+ case "COMPLETED": return b("assethandler.field.wo.status.completed", "Completed");
17
+ case "CANCELLED": return b("assethandler.field.wo.status.cancelled", "Cancelled");
18
+ default: return e;
19
+ }
20
+ }, F = () => {
21
+ let e = i(T);
22
+ if (!e.ok) {
23
+ O(!0);
24
+ return;
25
+ }
26
+ O(!1), y(m, {
27
+ ...C.trim() ? { notes: C.trim() } : {},
28
+ ...e.value === void 0 ? {} : { actualCost: e.value },
29
+ checklist: n(m.checklist, k)
30
+ }) && (S(!1), w(""), E(""));
31
+ };
32
+ return /* @__PURE__ */ u("li", {
33
+ className: "space-y-3 rounded-xl border border-border-seam p-3",
34
+ "data-testid": N,
35
+ children: [
36
+ /* @__PURE__ */ u("div", {
37
+ className: "flex items-start justify-between gap-3",
38
+ children: [/* @__PURE__ */ u("div", {
39
+ className: "min-w-0",
40
+ children: [/* @__PURE__ */ u("p", {
41
+ className: "flex items-center gap-2 truncate text-sm font-medium text-text-primary",
42
+ children: [/* @__PURE__ */ l(c, {
43
+ className: "h-4 w-4 shrink-0",
44
+ "aria-hidden": "true"
45
+ }), /* @__PURE__ */ l("span", {
46
+ className: "truncate",
47
+ children: m.title
48
+ })]
49
+ }), _ ? /* @__PURE__ */ l("p", {
50
+ className: "truncate text-xs text-text-secondary",
51
+ children: _
52
+ }) : null]
53
+ }), /* @__PURE__ */ u("span", {
54
+ "data-testid": `${N}-status`,
55
+ className: e("shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium", j ? M ? "bg-status-info-subtle text-status-info-text" : "bg-bg-sunken text-text-secondary" : "bg-status-success-subtle text-status-success-text"),
56
+ children: [P(h), g ? ` · ${b("assethandler.field.wo.waiting", "waiting to sync")}` : ""]
57
+ })]
58
+ }),
59
+ j && !x ? /* @__PURE__ */ u("div", {
60
+ className: "grid grid-cols-2 gap-2",
61
+ children: [/* @__PURE__ */ u(d, {
62
+ type: "button",
63
+ variant: "outline",
64
+ className: "h-12",
65
+ disabled: M,
66
+ onClick: () => v(m),
67
+ children: [/* @__PURE__ */ l(s, {
68
+ className: "h-4 w-4",
69
+ "aria-hidden": "true"
70
+ }), b("assethandler.field.wo.start", "Start work")]
71
+ }), /* @__PURE__ */ u(d, {
72
+ type: "button",
73
+ className: "h-12",
74
+ onClick: () => {
75
+ A(m.checklist.map((e) => e.completed)), S(!0);
76
+ },
77
+ children: [/* @__PURE__ */ l(o, {
78
+ className: "h-4 w-4",
79
+ "aria-hidden": "true"
80
+ }), b("assethandler.field.wo.complete", "Complete")]
81
+ })]
82
+ }) : null,
83
+ j && x ? /* @__PURE__ */ u("form", {
84
+ className: "space-y-3",
85
+ "data-testid": `${N}-complete-form`,
86
+ onSubmit: (e) => {
87
+ e.preventDefault(), F();
88
+ },
89
+ children: [
90
+ m.checklist.length > 0 ? /* @__PURE__ */ u("fieldset", {
91
+ className: "space-y-2",
92
+ children: [/* @__PURE__ */ l("legend", {
93
+ className: "text-sm font-medium text-text-primary",
94
+ children: b("assethandler.field.wo.checklist", "Checklist")
95
+ }), m.checklist.map((t, n) => /* @__PURE__ */ u("label", {
96
+ className: "flex min-h-12 cursor-pointer items-center gap-3 rounded border border-border-seam p-2.5",
97
+ children: [/* @__PURE__ */ l("input", {
98
+ type: "checkbox",
99
+ className: "h-5 w-5 accent-action-primary-bg",
100
+ checked: k[n] ?? !1,
101
+ onChange: (e) => A((t) => {
102
+ let r = [...t];
103
+ return r[n] = e.target.checked, r;
104
+ })
105
+ }), /* @__PURE__ */ l("span", {
106
+ className: e("text-sm", k[n] && "text-text-secondary line-through"),
107
+ children: t.task
108
+ })]
109
+ }, n))]
110
+ }) : null,
111
+ /* @__PURE__ */ u("div", {
112
+ className: "space-y-1",
113
+ children: [/* @__PURE__ */ l(p, {
114
+ htmlFor: `${N}-notes`,
115
+ children: b("assethandler.field.wo.notes", "Completion notes (optional)")
116
+ }), /* @__PURE__ */ l(f, {
117
+ id: `${N}-notes`,
118
+ value: C,
119
+ onChange: (e) => w(e.target.value),
120
+ placeholder: b("assethandler.field.wo.notesPlaceholder", "What was done, parts used…"),
121
+ className: "h-12"
122
+ })]
123
+ }),
124
+ /* @__PURE__ */ u("div", {
125
+ className: "space-y-1",
126
+ children: [
127
+ /* @__PURE__ */ l(p, {
128
+ htmlFor: `${N}-cost`,
129
+ children: b("assethandler.field.wo.cost", "Actual cost (optional)")
130
+ }),
131
+ /* @__PURE__ */ l(f, {
132
+ id: `${N}-cost`,
133
+ value: T,
134
+ onChange: (e) => E(e.target.value),
135
+ inputMode: "decimal",
136
+ "aria-invalid": D,
137
+ className: "h-12"
138
+ }),
139
+ D ? /* @__PURE__ */ l("p", {
140
+ role: "alert",
141
+ className: "text-xs text-status-error-text",
142
+ children: b("assethandler.field.wo.costInvalid", "Enter a cost of zero or more")
143
+ }) : null
144
+ ]
145
+ }),
146
+ /* @__PURE__ */ u("div", {
147
+ className: "grid grid-cols-2 gap-2",
148
+ children: [/* @__PURE__ */ l(d, {
149
+ type: "button",
150
+ variant: "outline",
151
+ className: "h-12",
152
+ onClick: () => S(!1),
153
+ children: b("assethandler.field.wo.cancel", "Cancel")
154
+ }), /* @__PURE__ */ l(d, {
155
+ type: "submit",
156
+ className: "h-12",
157
+ children: b("assethandler.field.wo.confirmComplete", "Mark complete")
158
+ })]
159
+ })
160
+ ]
161
+ }) : null
162
+ ]
163
+ });
164
+ }
165
+ //#endregion
166
+ export { m as FieldWorkOrderItem };
167
+
168
+ //# sourceMappingURL=FieldWorkOrderItem.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldWorkOrderItem.js","names":[],"sources":["../../../src/assethandler/components/FieldWorkOrderItem.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Button, Input, Label } from '@burdenoff/fe-libs/ui';\nimport { CheckCircle, Play, Wrench } from 'lucide-react';\nimport { useTr } from '../hooks/useTr';\nimport { cn } from '../utils/cn';\nimport {\n checklistChanges,\n isOpenWorkOrderStatus,\n parseActualCost,\n type FieldWorkOrder,\n} from '../utils/fieldWorkOrders';\nimport type { FieldChecklistChange } from '../utils/fieldQueue';\n\nexport interface FieldWorkOrderCompletion {\n notes?: string;\n actualCost?: number;\n checklist: FieldChecklistChange[];\n}\n\nexport interface FieldWorkOrderItemProps {\n workOrder: FieldWorkOrder;\n /** Status once every queued action on this work order has been applied. */\n projectedStatus: string;\n /** True while a queued action for this work order waits to sync. */\n queued: boolean;\n /** Display name of the linked asset, when known on this device. */\n assetName?: string;\n onStart: (workOrder: FieldWorkOrder) => void;\n /** Returns false when the completion could not be queued (form stays open). */\n onComplete: (workOrder: FieldWorkOrder, completion: FieldWorkOrderCompletion) => boolean;\n}\n\n/**\n * One work order in Field Mode (BOFF-10632): its status as the device projects\n * it, a one-tap \"Start work\", and an inline completion form (notes, actual\n * cost, checklist). Both actions are queued on the device by the caller.\n */\nexport function FieldWorkOrderItem({\n workOrder,\n projectedStatus,\n queued,\n assetName,\n onStart,\n onComplete,\n}: FieldWorkOrderItemProps) {\n const tr = useTr();\n const [completing, setCompleting] = useState(false);\n const [notes, setNotes] = useState('');\n const [cost, setCost] = useState('');\n const [costError, setCostError] = useState(false);\n const [ticks, setTicks] = useState<boolean[]>(() =>\n workOrder.checklist.map((item) => item.completed)\n );\n\n const open = isOpenWorkOrderStatus(projectedStatus);\n const inProgress = projectedStatus === 'IN_PROGRESS';\n const testId = `field-work-order-${workOrder.id}`;\n\n const statusLabel = (status: string) => {\n switch (status) {\n case 'DRAFT':\n return tr('assethandler.field.wo.status.draft', 'Draft');\n case 'APPROVED':\n return tr('assethandler.field.wo.status.approved', 'Approved');\n case 'SCHEDULED':\n return tr('assethandler.field.wo.status.scheduled', 'Scheduled');\n case 'IN_PROGRESS':\n return tr('assethandler.field.wo.status.inProgress', 'In progress');\n case 'COMPLETED':\n return tr('assethandler.field.wo.status.completed', 'Completed');\n case 'CANCELLED':\n return tr('assethandler.field.wo.status.cancelled', 'Cancelled');\n default:\n return status;\n }\n };\n\n const submit = () => {\n const parsed = parseActualCost(cost);\n if (!parsed.ok) {\n setCostError(true);\n return;\n }\n setCostError(false);\n const queuedOk = onComplete(workOrder, {\n ...(notes.trim() ? { notes: notes.trim() } : {}),\n ...(parsed.value !== undefined ? { actualCost: parsed.value } : {}),\n checklist: checklistChanges(workOrder.checklist, ticks),\n });\n if (queuedOk) {\n setCompleting(false);\n setNotes('');\n setCost('');\n }\n };\n\n return (\n <li className=\"space-y-3 rounded-xl border border-border-seam p-3\" data-testid={testId}>\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"flex items-center gap-2 truncate text-sm font-medium text-text-primary\">\n <Wrench className=\"h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n <span className=\"truncate\">{workOrder.title}</span>\n </p>\n {assetName ? <p className=\"truncate text-xs text-text-secondary\">{assetName}</p> : null}\n </div>\n <span\n data-testid={`${testId}-status`}\n className={cn(\n 'shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium',\n open\n ? inProgress\n ? 'bg-status-info-subtle text-status-info-text'\n : 'bg-bg-sunken text-text-secondary'\n : 'bg-status-success-subtle text-status-success-text'\n )}\n >\n {statusLabel(projectedStatus)}\n {queued ? ` · ${tr('assethandler.field.wo.waiting', 'waiting to sync')}` : ''}\n </span>\n </div>\n\n {open && !completing ? (\n <div className=\"grid grid-cols-2 gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n className=\"h-12\"\n disabled={inProgress}\n onClick={() => onStart(workOrder)}\n >\n <Play className=\"h-4 w-4\" aria-hidden=\"true\" />\n {tr('assethandler.field.wo.start', 'Start work')}\n </Button>\n <Button\n type=\"button\"\n className=\"h-12\"\n onClick={() => {\n // Start from the checklist as the device last synced it.\n setTicks(workOrder.checklist.map((item) => item.completed));\n setCompleting(true);\n }}\n >\n <CheckCircle className=\"h-4 w-4\" aria-hidden=\"true\" />\n {tr('assethandler.field.wo.complete', 'Complete')}\n </Button>\n </div>\n ) : null}\n\n {open && completing ? (\n <form\n className=\"space-y-3\"\n data-testid={`${testId}-complete-form`}\n onSubmit={(event) => {\n event.preventDefault();\n submit();\n }}\n >\n {workOrder.checklist.length > 0 ? (\n <fieldset className=\"space-y-2\">\n <legend className=\"text-sm font-medium text-text-primary\">\n {tr('assethandler.field.wo.checklist', 'Checklist')}\n </legend>\n {workOrder.checklist.map((item, index) => (\n <label\n key={index}\n className=\"flex min-h-12 cursor-pointer items-center gap-3 rounded border border-border-seam p-2.5\"\n >\n <input\n type=\"checkbox\"\n className=\"h-5 w-5 accent-action-primary-bg\"\n checked={ticks[index] ?? false}\n onChange={(e) =>\n setTicks((prev) => {\n const next = [...prev];\n next[index] = e.target.checked;\n return next;\n })\n }\n />\n <span\n className={cn('text-sm', ticks[index] && 'text-text-secondary line-through')}\n >\n {item.task}\n </span>\n </label>\n ))}\n </fieldset>\n ) : null}\n <div className=\"space-y-1\">\n <Label htmlFor={`${testId}-notes`}>\n {tr('assethandler.field.wo.notes', 'Completion notes (optional)')}\n </Label>\n <Input\n id={`${testId}-notes`}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={tr(\n 'assethandler.field.wo.notesPlaceholder',\n 'What was done, parts used…'\n )}\n className=\"h-12\"\n />\n </div>\n <div className=\"space-y-1\">\n <Label htmlFor={`${testId}-cost`}>\n {tr('assethandler.field.wo.cost', 'Actual cost (optional)')}\n </Label>\n <Input\n id={`${testId}-cost`}\n value={cost}\n onChange={(e) => setCost(e.target.value)}\n inputMode=\"decimal\"\n aria-invalid={costError}\n className=\"h-12\"\n />\n {costError ? (\n <p role=\"alert\" className=\"text-xs text-status-error-text\">\n {tr('assethandler.field.wo.costInvalid', 'Enter a cost of zero or more')}\n </p>\n ) : null}\n </div>\n <div className=\"grid grid-cols-2 gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n className=\"h-12\"\n onClick={() => setCompleting(false)}\n >\n {tr('assethandler.field.wo.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" className=\"h-12\">\n {tr('assethandler.field.wo.confirmComplete', 'Mark complete')}\n </Button>\n </div>\n </form>\n ) : null}\n </li>\n );\n}\n"],"mappings":"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"}
@@ -3,6 +3,9 @@ import { FieldAction, FieldSendOutcome, FlushResult, NewFieldAction, QueueStorag
3
3
 
4
4
  export declare const FIELD_CHECK_OUT_BY_TAG: import('graphql').DocumentNode;
5
5
  export declare const FIELD_CHECK_IN_BY_TAG: import('graphql').DocumentNode;
6
+ export declare const FIELD_START_WORK_ORDER: import('graphql').DocumentNode;
7
+ export declare const FIELD_SET_CHECKLIST_ITEM: import('graphql').DocumentNode;
8
+ export declare const FIELD_COMPLETE_WORK_ORDER: import('graphql').DocumentNode;
6
9
  /** How often a non-empty queue retries while the device reports online. */
7
10
  export declare const FIELD_SYNC_INTERVAL_MS = 30000;
8
11
  /** localStorage, or null where the browser blocks it (private mode, previews). */
@@ -1 +1 @@
1
- {"version":3,"file":"useFieldQueue.d.ts","sourceRoot":"","sources":["../../../src/assethandler/hooks/useFieldQueue.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAUL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,sBAAsB,gCAOlC,CAAC;AAEF,eAAO,MAAM,qBAAqB,gCAOjC,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C,kFAAkF;AAClF,wBAAgB,cAAc,IAAI,YAAY,GAAG,IAAI,CAMpD;AAMD,0EAA0E;AAC1E,wBAAgB,eAAe,IAAI,OAAO,CAYzC;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,GACnC,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,gBAAgB,CAAC,CA2BpD;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,wEAAwE;IACxE,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,6EAA6E;IAC7E,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,WAAW,CAAC;IAChD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,6EAA6E;IAC7E,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,MAAM,EACN,OAA0B,EAC1B,SAAS,GACV,EAAE,oBAAoB,GAAG,aAAa,CAoKtC"}
1
+ {"version":3,"file":"useFieldQueue.d.ts","sourceRoot":"","sources":["../../../src/assethandler/hooks/useFieldQueue.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAWL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,sBAAsB,gCAOlC,CAAC;AAEF,eAAO,MAAM,qBAAqB,gCAOjC,CAAC;AAEF,eAAO,MAAM,sBAAsB,gCAOlC,CAAC;AAEF,eAAO,MAAM,wBAAwB,gCAUpC,CAAC;AAEF,eAAO,MAAM,yBAAyB,gCAQrC,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C,kFAAkF;AAClF,wBAAgB,cAAc,IAAI,YAAY,GAAG,IAAI,CAMpD;AAMD,0EAA0E;AAC1E,wBAAgB,eAAe,IAAI,OAAO,CAYzC;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,GACnC,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAkCpD;AA4CD,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,wEAAwE;IACxE,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,6EAA6E;IAC7E,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,WAAW,CAAC;IAChD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,6EAA6E;IAC7E,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,MAAM,EACN,OAA0B,EAC1B,SAAS,GACV,EAAE,oBAAoB,GAAG,aAAa,CAoKtC"}
@@ -1,50 +1,80 @@
1
- import { classifySendError as e, enqueueFieldAction as t, fieldQueueStorageKey as n, flushFieldQueue as r, loadFieldQueue as i, pendingCount as a, removeFieldAction as o, retryFieldAction as s, saveFieldQueue as c } from "../utils/fieldQueue.js";
2
- import { useCallback as l, useEffect as u, useRef as d, useState as f } from "react";
3
- import { gql as p } from "@apollo/client";
1
+ import { classifySendError as e, enqueueFieldAction as t, fieldQueueStorageKey as n, flushFieldQueue as r, isWorkOrderActionKind as i, loadFieldQueue as a, pendingCount as o, removeFieldAction as s, retryFieldAction as c, saveFieldQueue as l } from "../utils/fieldQueue.js";
2
+ import { useCallback as u, useEffect as d, useRef as f, useState as p } from "react";
3
+ import { gql as m } from "@apollo/client";
4
4
  //#region src/assethandler/hooks/useFieldQueue.ts
5
- var m = p`
5
+ var h = m`
6
6
  mutation AssetHandlerFieldCheckOutByTag($input: CheckOutAssetByTagInput!) {
7
7
  checkOutAssetByTag(input: $input) {
8
8
  id
9
9
  status
10
10
  }
11
11
  }
12
- `, h = p`
12
+ `, g = m`
13
13
  mutation AssetHandlerFieldCheckInByTag($input: CheckInAssetByTagInput!) {
14
14
  checkInAssetByTag(input: $input) {
15
15
  id
16
16
  status
17
17
  }
18
18
  }
19
- `, g = 3e4;
20
- function _() {
19
+ `, _ = m`
20
+ mutation AssetHandlerFieldStartWorkOrder($id: ID!, $input: UpdateWorkOrderInput!) {
21
+ updateWorkOrder(id: $id, input: $input) {
22
+ id
23
+ status
24
+ }
25
+ }
26
+ `, v = m`
27
+ mutation AssetHandlerFieldSetChecklistItem(
28
+ $workOrderId: ID!
29
+ $index: Int!
30
+ $completed: Boolean!
31
+ ) {
32
+ setWorkOrderChecklistItem(workOrderId: $workOrderId, index: $index, completed: $completed) {
33
+ id
34
+ }
35
+ }
36
+ `, y = m`
37
+ mutation AssetHandlerFieldCompleteWorkOrder($id: ID!, $input: CompleteWorkOrderInput!) {
38
+ completeWorkOrder(id: $id, input: $input) {
39
+ id
40
+ status
41
+ completedAt
42
+ }
43
+ }
44
+ `, b = 3e4;
45
+ function x() {
21
46
  try {
22
47
  return typeof window < "u" && window.localStorage ? window.localStorage : null;
23
48
  } catch {
24
49
  return null;
25
50
  }
26
51
  }
27
- function v() {
52
+ function S() {
28
53
  return typeof navigator > "u" || navigator.onLine !== !1;
29
54
  }
30
- function y() {
31
- let [e, t] = f(v);
32
- return u(() => {
33
- let e = () => t(v());
55
+ function C() {
56
+ let [e, t] = p(S);
57
+ return d(() => {
58
+ let e = () => t(S());
34
59
  return window.addEventListener("online", e), window.addEventListener("offline", e), () => {
35
60
  window.removeEventListener("online", e), window.removeEventListener("offline", e);
36
61
  };
37
62
  }, []), e;
38
63
  }
39
- function b(t) {
64
+ function w(t) {
40
65
  return async (n) => {
66
+ if (i(n.kind) && !n.workOrderId) return {
67
+ ok: !1,
68
+ retryable: !1,
69
+ message: "Queued work-order action has no target"
70
+ };
41
71
  let r = {
42
72
  clientMutationId: n.id,
43
73
  scannedAt: n.scannedAt
44
74
  };
45
75
  try {
46
- return n.kind === "CHECK_OUT" ? await t.mutate({
47
- mutation: m,
76
+ return n.kind === "WO_START" || n.kind === "WO_COMPLETE" ? await T(t, n) : n.kind === "CHECK_OUT" ? await t.mutate({
77
+ mutation: h,
48
78
  variables: { input: {
49
79
  tagId: n.tagId,
50
80
  assignedTo: n.assignedTo ?? "",
@@ -52,7 +82,7 @@ function b(t) {
52
82
  ...r
53
83
  } }
54
84
  }) : await t.mutate({
55
- mutation: h,
85
+ mutation: g,
56
86
  variables: { input: {
57
87
  tagId: n.tagId,
58
88
  notes: n.notes ?? null,
@@ -64,30 +94,66 @@ function b(t) {
64
94
  }
65
95
  };
66
96
  }
67
- function x({ workspaceId: e, client: p, storage: m = _(), onFlushed: h }) {
68
- let v = n(e), x = y(), [S, C] = f(() => i(m, v)), [w, T] = f(!1), [E, D] = f(null), O = d(null), [k, A] = f(!0), j = d(S), M = d(!0), N = d(!1), P = d(!0);
69
- u(() => (P.current = !0, () => {
97
+ async function T(e, t) {
98
+ let n = t.workOrderId ?? "";
99
+ if (t.kind === "WO_START") {
100
+ await e.mutate({
101
+ mutation: _,
102
+ variables: {
103
+ id: n,
104
+ input: {
105
+ status: "IN_PROGRESS",
106
+ clientMutationId: t.id
107
+ }
108
+ }
109
+ });
110
+ return;
111
+ }
112
+ for (let r of t.checklist ?? []) await e.mutate({
113
+ mutation: v,
114
+ variables: {
115
+ workOrderId: n,
116
+ index: r.index,
117
+ completed: r.completed
118
+ }
119
+ });
120
+ await e.mutate({
121
+ mutation: y,
122
+ variables: {
123
+ id: n,
124
+ input: {
125
+ clientMutationId: t.id,
126
+ completedAt: t.scannedAt,
127
+ ...t.actualCost === void 0 ? {} : { actualCost: t.actualCost },
128
+ ...t.notes ? { notes: t.notes } : {}
129
+ }
130
+ }
131
+ });
132
+ }
133
+ function E({ workspaceId: e, client: i, storage: m = x(), onFlushed: h }) {
134
+ let g = n(e), _ = C(), [v, y] = p(() => a(m, g)), [S, T] = p(!1), [E, D] = p(null), O = f(null), [k, A] = p(!0), j = f(v), M = f(!0), N = f(!1), P = f(!0);
135
+ d(() => (P.current = !0, () => {
70
136
  P.current = !1;
71
137
  }), []);
72
- let F = d(m), I = d(h), L = d(b(p));
73
- u(() => {
74
- F.current = m, I.current = h, L.current = b(p);
138
+ let F = f(m), I = f(h), L = f(w(i));
139
+ d(() => {
140
+ F.current = m, I.current = h, L.current = w(i);
75
141
  });
76
- let R = l((e) => {
77
- j.current = e, C(e);
78
- let t = c(F.current, v, e);
142
+ let R = u((e) => {
143
+ j.current = e, y(e);
144
+ let t = l(F.current, g, e);
79
145
  M.current = t, A(t);
80
- }, [v]), z = l(() => !F.current || !M.current ? j.current : i(F.current, v), [v]);
81
- u(() => {
146
+ }, [g]), z = u(() => !F.current || !M.current ? j.current : a(F.current, g), [g]);
147
+ d(() => {
82
148
  let e = (e) => {
83
- if (e.key !== v) return;
84
- let t = i(F.current, v);
85
- j.current = t, C(t);
149
+ if (e.key !== g) return;
150
+ let t = a(F.current, g);
151
+ j.current = t, y(t);
86
152
  };
87
153
  return window.addEventListener("storage", e), () => window.removeEventListener("storage", e);
88
- }, [v]);
89
- let B = l(async () => {
90
- if (!(N.current || a(j.current) === 0)) {
154
+ }, [g]);
155
+ let B = u(async () => {
156
+ if (!(N.current || o(j.current) === 0)) {
91
157
  N.current = !0, T(!0);
92
158
  try {
93
159
  let e = await r(j.current, async (e) => {
@@ -99,7 +165,7 @@ function x({ workspaceId: e, client: p, storage: m = _(), onFlushed: h }) {
99
165
  }
100
166
  }, (e) => z().some((t) => t.id === e && t.state === "pending")), t = new Set(e.synced.map((e) => e.id)), n = new Map(e.attempted.map((e) => [e.id, e])), i = z().filter((e) => !t.has(e.id)).map((e) => n.get(e.id) ?? e);
101
167
  if (!P.current) {
102
- c(F.current, v, i);
168
+ l(F.current, g, i);
103
169
  return;
104
170
  }
105
171
  R(i), (e.synced.length > 0 || e.rejected.length > 0) && I.current?.(e);
@@ -110,31 +176,31 @@ function x({ workspaceId: e, client: p, storage: m = _(), onFlushed: h }) {
110
176
  }, [
111
177
  R,
112
178
  z,
113
- v
114
- ]), V = l((e) => {
179
+ g
180
+ ]), V = u((e) => {
115
181
  let n = t(z(), e);
116
182
  return R(n), n[n.length - 1];
117
- }, [R, z]), H = l((e) => {
118
- e !== O.current && R(o(z(), e));
119
- }, [R, z]), U = l((e) => R(s(z(), e)), [R, z]), W = a(S);
120
- return u(() => {
121
- if (!x || W === 0) return;
183
+ }, [R, z]), H = u((e) => {
184
+ e !== O.current && R(s(z(), e));
185
+ }, [R, z]), U = u((e) => R(c(z(), e)), [R, z]), W = o(v);
186
+ return d(() => {
187
+ if (!_ || W === 0) return;
122
188
  B();
123
- let e = window.setInterval(() => void B(), g), t = () => {
189
+ let e = window.setInterval(() => void B(), b), t = () => {
124
190
  document.visibilityState === "visible" && B();
125
191
  };
126
192
  return document.addEventListener("visibilitychange", t), () => {
127
193
  window.clearInterval(e), document.removeEventListener("visibilitychange", t);
128
194
  };
129
195
  }, [
130
- x,
196
+ _,
131
197
  W,
132
198
  B
133
199
  ]), {
134
- queue: S,
200
+ queue: v,
135
201
  pending: W,
136
- online: x,
137
- syncing: w,
202
+ online: _,
203
+ syncing: S,
138
204
  sendingId: E,
139
205
  persisted: k,
140
206
  enqueue: V,
@@ -144,6 +210,6 @@ function x({ workspaceId: e, client: p, storage: m = _(), onFlushed: h }) {
144
210
  };
145
211
  }
146
212
  //#endregion
147
- export { _ as browserStorage, x as useFieldQueue };
213
+ export { x as browserStorage, E as useFieldQueue };
148
214
 
149
215
  //# sourceMappingURL=useFieldQueue.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useFieldQueue.js","names":[],"sources":["../../../src/assethandler/hooks/useFieldQueue.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { gql, type ApolloClient } from '@apollo/client';\nimport {\n classifySendError,\n enqueueFieldAction,\n fieldQueueStorageKey,\n flushFieldQueue,\n loadFieldQueue,\n pendingCount,\n removeFieldAction,\n retryFieldAction,\n saveFieldQueue,\n type FieldAction,\n type FieldSendOutcome,\n type FlushResult,\n type NewFieldAction,\n type QueueStorage,\n} from '../utils/fieldQueue';\n\nexport const FIELD_CHECK_OUT_BY_TAG = gql`\n mutation AssetHandlerFieldCheckOutByTag($input: CheckOutAssetByTagInput!) {\n checkOutAssetByTag(input: $input) {\n id\n status\n }\n }\n`;\n\nexport const FIELD_CHECK_IN_BY_TAG = gql`\n mutation AssetHandlerFieldCheckInByTag($input: CheckInAssetByTagInput!) {\n checkInAssetByTag(input: $input) {\n id\n status\n }\n }\n`;\n\n/** How often a non-empty queue retries while the device reports online. */\nexport const FIELD_SYNC_INTERVAL_MS = 30_000;\n\n/** localStorage, or null where the browser blocks it (private mode, previews). */\nexport function browserStorage(): QueueStorage | null {\n try {\n return typeof window !== 'undefined' && window.localStorage ? window.localStorage : null;\n } catch {\n return null;\n }\n}\n\nfunction readOnline(): boolean {\n return typeof navigator === 'undefined' || navigator.onLine !== false;\n}\n\n/** Tracks `navigator.onLine` through the window online/offline events. */\nexport function useOnlineStatus(): boolean {\n const [online, setOnline] = useState(readOnline);\n useEffect(() => {\n const update = () => setOnline(readOnline());\n window.addEventListener('online', update);\n window.addEventListener('offline', update);\n return () => {\n window.removeEventListener('online', update);\n window.removeEventListener('offline', update);\n };\n }, []);\n return online;\n}\n\n/** Sends one queued action to the gateway with its idempotency key. */\nexport function createFieldSender(\n client: Pick<ApolloClient, 'mutate'>\n): (action: FieldAction) => Promise<FieldSendOutcome> {\n return async (action) => {\n const replay = { clientMutationId: action.id, scannedAt: action.scannedAt };\n try {\n if (action.kind === 'CHECK_OUT') {\n await client.mutate({\n mutation: FIELD_CHECK_OUT_BY_TAG,\n variables: {\n input: {\n tagId: action.tagId,\n assignedTo: action.assignedTo ?? '',\n notes: action.notes ?? null,\n ...replay,\n },\n },\n });\n } else {\n await client.mutate({\n mutation: FIELD_CHECK_IN_BY_TAG,\n variables: { input: { tagId: action.tagId, notes: action.notes ?? null, ...replay } },\n });\n }\n return { ok: true };\n } catch (error) {\n return classifySendError(error);\n }\n };\n}\n\nexport interface UseFieldQueueOptions {\n workspaceId?: string | null;\n client: Pick<ApolloClient, 'mutate'>;\n storage?: QueueStorage | null;\n /** Called after a flush that synced or rejected at least one action. */\n onFlushed?: (result: FlushResult) => void;\n}\n\nexport interface FieldQueueApi {\n queue: FieldAction[];\n pending: number;\n online: boolean;\n syncing: boolean;\n /**\n * The action whose mutation is in flight right now, or null. Its request\n * cannot be recalled, so it cannot be discarded until it settles.\n */\n sendingId: string | null;\n /** False when the queue could not be persisted (storage blocked or full). */\n persisted: boolean;\n enqueue: (input: NewFieldAction) => FieldAction;\n syncNow: () => Promise<void>;\n /** Discards a queued action; a no-op for the action currently being sent. */\n remove: (id: string) => void;\n retry: (id: string) => void;\n}\n\n/**\n * React binding for the offline custody queue: persistence per workspace,\n * replay on reconnect / tab focus / a periodic timer, and cross-tab updates.\n * The queue is read once per mount; callers remount (React `key`) when the\n * workspace changes so each workspace keeps its own queue.\n */\nexport function useFieldQueue({\n workspaceId,\n client,\n storage = browserStorage(),\n onFlushed,\n}: UseFieldQueueOptions): FieldQueueApi {\n const key = fieldQueueStorageKey(workspaceId);\n const online = useOnlineStatus();\n const [queue, setQueueState] = useState<FieldAction[]>(() => loadFieldQueue(storage, key));\n const [syncing, setSyncing] = useState(false);\n const [sendingId, setSendingId] = useState<string | null>(null);\n const sendingRef = useRef<string | null>(null);\n const [persisted, setPersisted] = useState(true);\n const queueRef = useRef(queue);\n // Whether this instance's last write reached storage. While it did, the\n // persisted copy is the shared source of truth across tabs and instances.\n const persistedRef = useRef(true);\n const flushingRef = useRef(false);\n const mountedRef = useRef(true);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n const storageRef = useRef(storage);\n const onFlushedRef = useRef(onFlushed);\n const sendRef = useRef(createFieldSender(client));\n // Latest-value refs: the flush loop and listeners read these, so they are\n // refreshed after every render instead of re-subscribing.\n useEffect(() => {\n storageRef.current = storage;\n onFlushedRef.current = onFlushed;\n sendRef.current = createFieldSender(client);\n });\n\n const commit = useCallback(\n (next: FieldAction[]) => {\n queueRef.current = next;\n setQueueState(next);\n const saved = saveFieldQueue(storageRef.current, key, next);\n persistedRef.current = saved;\n setPersisted(saved);\n },\n [key]\n );\n\n // The queue a flush must reconcile against. Every successful commit writes\n // through to storage, so whenever storage is available and our last write\n // landed, the persisted copy is authoritative: it already holds what other\n // tabs (whose storage events may not have been delivered yet) and newer\n // instances of this hook wrote while a request was in flight. Only when\n // storage is blocked or full does the in-memory queue win.\n const currentQueue = useCallback((): FieldAction[] => {\n if (!storageRef.current || !persistedRef.current) return queueRef.current;\n return loadFieldQueue(storageRef.current, key);\n }, [key]);\n\n // Another tab changed the queue: adopt its copy.\n useEffect(() => {\n const onStorage = (event: StorageEvent) => {\n if (event.key !== key) return;\n const loaded = loadFieldQueue(storageRef.current, key);\n queueRef.current = loaded;\n setQueueState(loaded);\n };\n window.addEventListener('storage', onStorage);\n return () => window.removeEventListener('storage', onStorage);\n }, [key]);\n\n const syncNow = useCallback(async () => {\n if (flushingRef.current || pendingCount(queueRef.current) === 0) return;\n flushingRef.current = true;\n setSyncing(true);\n try {\n // Track the action on the wire so the UI can lock it: a sent request\n // cannot be recalled, so discarding it would hide custody the server\n // may still record.\n const send = async (action: FieldAction): Promise<FieldSendOutcome> => {\n sendingRef.current = action.id;\n if (mountedRef.current) setSendingId(action.id);\n try {\n return await sendRef.current(action);\n } finally {\n sendingRef.current = null;\n if (mountedRef.current) setSendingId(null);\n }\n };\n const result = await flushFieldQueue(queueRef.current, send, (id) =>\n currentQueue().some((a) => a.id === id && a.state === 'pending')\n );\n // Merge onto the current queue: actions enqueued, removed or retried\n // during the flush must survive it, so only the rows this flush actually\n // sent take its result; every other row keeps its current state.\n const synced = new Set(result.synced.map((a) => a.id));\n const updated = new Map(result.attempted.map((a) => [a.id, a]));\n const merged = currentQueue()\n .filter((a) => !synced.has(a.id))\n .map((a) => updated.get(a.id) ?? a);\n if (!mountedRef.current) {\n // Persist only: there is no state to update, and the queue a newer\n // instance enqueued is already part of `merged`.\n saveFieldQueue(storageRef.current, key, merged);\n return;\n }\n commit(merged);\n if (result.synced.length > 0 || result.rejected.length > 0) {\n onFlushedRef.current?.(result);\n }\n } finally {\n flushingRef.current = false;\n if (mountedRef.current) setSyncing(false);\n }\n }, [commit, currentQueue, key]);\n\n const enqueue = useCallback(\n (input: NewFieldAction) => {\n const next = enqueueFieldAction(currentQueue(), input);\n commit(next);\n return next[next.length - 1];\n },\n [commit, currentQueue]\n );\n\n // Local edits start from the current queue too, so a write from another tab\n // whose storage event has not arrived yet is merged instead of overwritten.\n const remove = useCallback(\n (id: string) => {\n if (id === sendingRef.current) return;\n commit(removeFieldAction(currentQueue(), id));\n },\n [commit, currentQueue]\n );\n\n const retry = useCallback(\n (id: string) => commit(retryFieldAction(currentQueue(), id)),\n [commit, currentQueue]\n );\n\n const pending = pendingCount(queue);\n\n // Replay whenever connectivity returns, the page regains focus, the queue\n // gains work while online, or on a slow timer while work remains.\n useEffect(() => {\n if (!online || pending === 0) return;\n void syncNow();\n const timer = window.setInterval(() => void syncNow(), FIELD_SYNC_INTERVAL_MS);\n const onVisible = () => {\n if (document.visibilityState === 'visible') void syncNow();\n };\n document.addEventListener('visibilitychange', onVisible);\n return () => {\n window.clearInterval(timer);\n document.removeEventListener('visibilitychange', onVisible);\n };\n }, [online, pending, syncNow]);\n\n return {\n queue,\n pending,\n online,\n syncing,\n sendingId,\n persisted,\n enqueue,\n syncNow,\n remove,\n retry,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useFieldQueue.js","names":[],"sources":["../../../src/assethandler/hooks/useFieldQueue.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { gql, type ApolloClient } from '@apollo/client';\nimport {\n classifySendError,\n enqueueFieldAction,\n fieldQueueStorageKey,\n flushFieldQueue,\n isWorkOrderActionKind,\n loadFieldQueue,\n pendingCount,\n removeFieldAction,\n retryFieldAction,\n saveFieldQueue,\n type FieldAction,\n type FieldSendOutcome,\n type FlushResult,\n type NewFieldAction,\n type QueueStorage,\n} from '../utils/fieldQueue';\n\nexport const FIELD_CHECK_OUT_BY_TAG = gql`\n mutation AssetHandlerFieldCheckOutByTag($input: CheckOutAssetByTagInput!) {\n checkOutAssetByTag(input: $input) {\n id\n status\n }\n }\n`;\n\nexport const FIELD_CHECK_IN_BY_TAG = gql`\n mutation AssetHandlerFieldCheckInByTag($input: CheckInAssetByTagInput!) {\n checkInAssetByTag(input: $input) {\n id\n status\n }\n }\n`;\n\nexport const FIELD_START_WORK_ORDER = gql`\n mutation AssetHandlerFieldStartWorkOrder($id: ID!, $input: UpdateWorkOrderInput!) {\n updateWorkOrder(id: $id, input: $input) {\n id\n status\n }\n }\n`;\n\nexport const FIELD_SET_CHECKLIST_ITEM = gql`\n mutation AssetHandlerFieldSetChecklistItem(\n $workOrderId: ID!\n $index: Int!\n $completed: Boolean!\n ) {\n setWorkOrderChecklistItem(workOrderId: $workOrderId, index: $index, completed: $completed) {\n id\n }\n }\n`;\n\nexport const FIELD_COMPLETE_WORK_ORDER = gql`\n mutation AssetHandlerFieldCompleteWorkOrder($id: ID!, $input: CompleteWorkOrderInput!) {\n completeWorkOrder(id: $id, input: $input) {\n id\n status\n completedAt\n }\n }\n`;\n\n/** How often a non-empty queue retries while the device reports online. */\nexport const FIELD_SYNC_INTERVAL_MS = 30_000;\n\n/** localStorage, or null where the browser blocks it (private mode, previews). */\nexport function browserStorage(): QueueStorage | null {\n try {\n return typeof window !== 'undefined' && window.localStorage ? window.localStorage : null;\n } catch {\n return null;\n }\n}\n\nfunction readOnline(): boolean {\n return typeof navigator === 'undefined' || navigator.onLine !== false;\n}\n\n/** Tracks `navigator.onLine` through the window online/offline events. */\nexport function useOnlineStatus(): boolean {\n const [online, setOnline] = useState(readOnline);\n useEffect(() => {\n const update = () => setOnline(readOnline());\n window.addEventListener('online', update);\n window.addEventListener('offline', update);\n return () => {\n window.removeEventListener('online', update);\n window.removeEventListener('offline', update);\n };\n }, []);\n return online;\n}\n\n/** Sends one queued action to the gateway with its idempotency key. */\nexport function createFieldSender(\n client: Pick<ApolloClient, 'mutate'>\n): (action: FieldAction) => Promise<FieldSendOutcome> {\n return async (action) => {\n if (isWorkOrderActionKind(action.kind) && !action.workOrderId) {\n // Unreachable for queues written by this module (enqueue and load both\n // refuse it); replaying cannot fix it, so park it instead of retrying.\n return { ok: false, retryable: false, message: 'Queued work-order action has no target' };\n }\n const replay = { clientMutationId: action.id, scannedAt: action.scannedAt };\n try {\n if (action.kind === 'WO_START' || action.kind === 'WO_COMPLETE') {\n await sendWorkOrderAction(client, action);\n } else if (action.kind === 'CHECK_OUT') {\n await client.mutate({\n mutation: FIELD_CHECK_OUT_BY_TAG,\n variables: {\n input: {\n tagId: action.tagId,\n assignedTo: action.assignedTo ?? '',\n notes: action.notes ?? null,\n ...replay,\n },\n },\n });\n } else {\n await client.mutate({\n mutation: FIELD_CHECK_IN_BY_TAG,\n variables: { input: { tagId: action.tagId, notes: action.notes ?? null, ...replay } },\n });\n }\n return { ok: true };\n } catch (error) {\n return classifySendError(error);\n }\n };\n}\n\n/**\n * Replays a queued work-order action. A start is one idempotent\n * `updateWorkOrder`. A completion first applies the checklist ticks captured\n * on site (each sets an item to a fixed value, so repeating it is harmless,\n * also after the work order closed), then `completeWorkOrder` with the\n * action's key and the time the technician finished on the device.\n */\nasync function sendWorkOrderAction(\n client: Pick<ApolloClient, 'mutate'>,\n action: FieldAction\n): Promise<void> {\n const workOrderId = action.workOrderId ?? '';\n if (action.kind === 'WO_START') {\n await client.mutate({\n mutation: FIELD_START_WORK_ORDER,\n variables: {\n id: workOrderId,\n input: { status: 'IN_PROGRESS', clientMutationId: action.id },\n },\n });\n return;\n }\n for (const change of action.checklist ?? []) {\n await client.mutate({\n mutation: FIELD_SET_CHECKLIST_ITEM,\n variables: { workOrderId, index: change.index, completed: change.completed },\n });\n }\n await client.mutate({\n mutation: FIELD_COMPLETE_WORK_ORDER,\n variables: {\n id: workOrderId,\n input: {\n clientMutationId: action.id,\n completedAt: action.scannedAt,\n ...(action.actualCost !== undefined ? { actualCost: action.actualCost } : {}),\n ...(action.notes ? { notes: action.notes } : {}),\n },\n },\n });\n}\n\nexport interface UseFieldQueueOptions {\n workspaceId?: string | null;\n client: Pick<ApolloClient, 'mutate'>;\n storage?: QueueStorage | null;\n /** Called after a flush that synced or rejected at least one action. */\n onFlushed?: (result: FlushResult) => void;\n}\n\nexport interface FieldQueueApi {\n queue: FieldAction[];\n pending: number;\n online: boolean;\n syncing: boolean;\n /**\n * The action whose mutation is in flight right now, or null. Its request\n * cannot be recalled, so it cannot be discarded until it settles.\n */\n sendingId: string | null;\n /** False when the queue could not be persisted (storage blocked or full). */\n persisted: boolean;\n enqueue: (input: NewFieldAction) => FieldAction;\n syncNow: () => Promise<void>;\n /** Discards a queued action; a no-op for the action currently being sent. */\n remove: (id: string) => void;\n retry: (id: string) => void;\n}\n\n/**\n * React binding for the offline custody queue: persistence per workspace,\n * replay on reconnect / tab focus / a periodic timer, and cross-tab updates.\n * The queue is read once per mount; callers remount (React `key`) when the\n * workspace changes so each workspace keeps its own queue.\n */\nexport function useFieldQueue({\n workspaceId,\n client,\n storage = browserStorage(),\n onFlushed,\n}: UseFieldQueueOptions): FieldQueueApi {\n const key = fieldQueueStorageKey(workspaceId);\n const online = useOnlineStatus();\n const [queue, setQueueState] = useState<FieldAction[]>(() => loadFieldQueue(storage, key));\n const [syncing, setSyncing] = useState(false);\n const [sendingId, setSendingId] = useState<string | null>(null);\n const sendingRef = useRef<string | null>(null);\n const [persisted, setPersisted] = useState(true);\n const queueRef = useRef(queue);\n // Whether this instance's last write reached storage. While it did, the\n // persisted copy is the shared source of truth across tabs and instances.\n const persistedRef = useRef(true);\n const flushingRef = useRef(false);\n const mountedRef = useRef(true);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n const storageRef = useRef(storage);\n const onFlushedRef = useRef(onFlushed);\n const sendRef = useRef(createFieldSender(client));\n // Latest-value refs: the flush loop and listeners read these, so they are\n // refreshed after every render instead of re-subscribing.\n useEffect(() => {\n storageRef.current = storage;\n onFlushedRef.current = onFlushed;\n sendRef.current = createFieldSender(client);\n });\n\n const commit = useCallback(\n (next: FieldAction[]) => {\n queueRef.current = next;\n setQueueState(next);\n const saved = saveFieldQueue(storageRef.current, key, next);\n persistedRef.current = saved;\n setPersisted(saved);\n },\n [key]\n );\n\n // The queue a flush must reconcile against. Every successful commit writes\n // through to storage, so whenever storage is available and our last write\n // landed, the persisted copy is authoritative: it already holds what other\n // tabs (whose storage events may not have been delivered yet) and newer\n // instances of this hook wrote while a request was in flight. Only when\n // storage is blocked or full does the in-memory queue win.\n const currentQueue = useCallback((): FieldAction[] => {\n if (!storageRef.current || !persistedRef.current) return queueRef.current;\n return loadFieldQueue(storageRef.current, key);\n }, [key]);\n\n // Another tab changed the queue: adopt its copy.\n useEffect(() => {\n const onStorage = (event: StorageEvent) => {\n if (event.key !== key) return;\n const loaded = loadFieldQueue(storageRef.current, key);\n queueRef.current = loaded;\n setQueueState(loaded);\n };\n window.addEventListener('storage', onStorage);\n return () => window.removeEventListener('storage', onStorage);\n }, [key]);\n\n const syncNow = useCallback(async () => {\n if (flushingRef.current || pendingCount(queueRef.current) === 0) return;\n flushingRef.current = true;\n setSyncing(true);\n try {\n // Track the action on the wire so the UI can lock it: a sent request\n // cannot be recalled, so discarding it would hide custody the server\n // may still record.\n const send = async (action: FieldAction): Promise<FieldSendOutcome> => {\n sendingRef.current = action.id;\n if (mountedRef.current) setSendingId(action.id);\n try {\n return await sendRef.current(action);\n } finally {\n sendingRef.current = null;\n if (mountedRef.current) setSendingId(null);\n }\n };\n const result = await flushFieldQueue(queueRef.current, send, (id) =>\n currentQueue().some((a) => a.id === id && a.state === 'pending')\n );\n // Merge onto the current queue: actions enqueued, removed or retried\n // during the flush must survive it, so only the rows this flush actually\n // sent take its result; every other row keeps its current state.\n const synced = new Set(result.synced.map((a) => a.id));\n const updated = new Map(result.attempted.map((a) => [a.id, a]));\n const merged = currentQueue()\n .filter((a) => !synced.has(a.id))\n .map((a) => updated.get(a.id) ?? a);\n if (!mountedRef.current) {\n // Persist only: there is no state to update, and the queue a newer\n // instance enqueued is already part of `merged`.\n saveFieldQueue(storageRef.current, key, merged);\n return;\n }\n commit(merged);\n if (result.synced.length > 0 || result.rejected.length > 0) {\n onFlushedRef.current?.(result);\n }\n } finally {\n flushingRef.current = false;\n if (mountedRef.current) setSyncing(false);\n }\n }, [commit, currentQueue, key]);\n\n const enqueue = useCallback(\n (input: NewFieldAction) => {\n const next = enqueueFieldAction(currentQueue(), input);\n commit(next);\n return next[next.length - 1];\n },\n [commit, currentQueue]\n );\n\n // Local edits start from the current queue too, so a write from another tab\n // whose storage event has not arrived yet is merged instead of overwritten.\n const remove = useCallback(\n (id: string) => {\n if (id === sendingRef.current) return;\n commit(removeFieldAction(currentQueue(), id));\n },\n [commit, currentQueue]\n );\n\n const retry = useCallback(\n (id: string) => commit(retryFieldAction(currentQueue(), id)),\n [commit, currentQueue]\n );\n\n const pending = pendingCount(queue);\n\n // Replay whenever connectivity returns, the page regains focus, the queue\n // gains work while online, or on a slow timer while work remains.\n useEffect(() => {\n if (!online || pending === 0) return;\n void syncNow();\n const timer = window.setInterval(() => void syncNow(), FIELD_SYNC_INTERVAL_MS);\n const onVisible = () => {\n if (document.visibilityState === 'visible') void syncNow();\n };\n document.addEventListener('visibilitychange', onVisible);\n return () => {\n window.clearInterval(timer);\n document.removeEventListener('visibilitychange', onVisible);\n };\n }, [online, pending, syncNow]);\n\n return {\n queue,\n pending,\n online,\n syncing,\n sendingId,\n persisted,\n enqueue,\n syncNow,\n remove,\n retry,\n };\n}\n"],"mappings":"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"}
@@ -13,5 +13,13 @@
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
+ /**
17
+ * The technician's open work orders (BOFF-10632), cached per workspace so
18
+ * they can be started and completed offline. `assignedTo` is free text, so
19
+ * every identity the technician is known by is passed to the filter.
20
+ */
21
+ export declare const FIELD_WORK_ORDERS: import('graphql').DocumentNode;
22
+ /** Online lookup of one asset by its printed tag (scan or deep link). */
23
+ export declare const FIELD_ASSET_BY_TAG: import('graphql').DocumentNode;
16
24
  export declare function FieldModePage(): import("react").JSX.Element;
17
25
  //# sourceMappingURL=FieldModePage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FieldModePage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/FieldModePage.tsx"],"names":[],"mappings":"AAyCA;;;;;;GAMG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,gCAYxB,CAAC;AA0FF,wBAAgB,aAAa,gCAK5B"}
1
+ {"version":3,"file":"FieldModePage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/FieldModePage.tsx"],"names":[],"mappings":"AA4DA;;;;;;GAMG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,gCAYxB,CAAC;AAwBF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,gCAY7B,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,gCAY9B,CAAC;AAsDF,wBAAgB,aAAa,gCAK5B"}