@olenbetong/synergi-react 2.1.2 → 2.3.0

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.
Files changed (197) hide show
  1. package/dist/esm/ob.react.css +1189 -27
  2. package/dist/esm/ob.react.js +45612 -708
  3. package/dist/esm/ob.react.min.css +24 -1
  4. package/dist/esm/ob.react.min.css.map +3 -3
  5. package/dist/esm/ob.react.min.js +253 -1
  6. package/dist/esm/ob.react.min.js.map +4 -4
  7. package/dist/iife/ob.react.css +1189 -27
  8. package/dist/iife/ob.react.js +45625 -720
  9. package/dist/iife/ob.react.min.css +24 -1
  10. package/dist/iife/ob.react.min.css.map +3 -3
  11. package/dist/iife/ob.react.min.js +253 -1
  12. package/dist/iife/ob.react.min.js.map +4 -4
  13. package/dist/styles/styles.css +1 -1
  14. package/dist/styles/styles.css.map +1 -1
  15. package/{src → es/components}/ColorCard/index.css +10 -6
  16. package/es/{DateNavigator → components/DateNavigator}/index.js +2 -0
  17. package/es/components/FilePond.d.ts +44 -0
  18. package/es/components/FilePond.js +205 -0
  19. package/es/{PageBanner → components/PageBanner}/index.js +0 -1
  20. package/es/{Sidebar → components/Sidebar}/index.d.ts +2 -2
  21. package/es/{SplitContainer → components/SplitContainer}/index.js +3 -3
  22. package/es/{ValueToggleGroup → components/ValueToggleGroup}/index.css +18 -16
  23. package/es/{ValueToggleGroup → components/ValueToggleGroup}/index.js +4 -2
  24. package/es/elements/ElementDamage/DamageFormButtons.d.ts +9 -0
  25. package/es/elements/ElementDamage/DamageFormButtons.js +7 -0
  26. package/es/elements/ElementDamage/DescriptionInput.d.ts +14 -0
  27. package/es/elements/ElementDamage/DescriptionInput.js +44 -0
  28. package/es/elements/ElementDamage/DuplicateDamage/DesktopView.d.ts +7 -0
  29. package/es/elements/ElementDamage/DuplicateDamage/DesktopView.js +16 -0
  30. package/es/elements/ElementDamage/DuplicateDamage/DublicateElementDamageDialog.d.ts +10 -0
  31. package/es/elements/ElementDamage/DuplicateDamage/DublicateElementDamageDialog.js +12 -0
  32. package/es/elements/ElementDamage/DuplicateDamage/PhoneView.d.ts +6 -0
  33. package/es/elements/ElementDamage/DuplicateDamage/PhoneView.js +8 -0
  34. package/es/elements/ElementDamage/DuplicateDamage/index.d.ts +36 -0
  35. package/es/elements/ElementDamage/DuplicateDamage/index.js +40 -0
  36. package/es/elements/ElementDamage/ElementNumberTextField.d.ts +10 -0
  37. package/es/elements/ElementDamage/ElementNumberTextField.js +18 -0
  38. package/es/elements/ElementDamage/HiddenDamageFields.d.ts +6 -0
  39. package/es/elements/ElementDamage/HiddenDamageFields.js +4 -0
  40. package/es/elements/ElementDamage/RegisterFix.d.ts +14 -0
  41. package/es/elements/ElementDamage/RegisterFix.js +45 -0
  42. package/es/elements/ElementDamage/SubmitButtons.d.ts +20 -0
  43. package/es/elements/ElementDamage/SubmitButtons.js +7 -0
  44. package/es/elements/ElementDamage/TransportElementSelect.d.ts +7 -0
  45. package/es/elements/ElementDamage/TransportElementSelect.js +16 -0
  46. package/es/elements/ElementDamage/index.css +3 -0
  47. package/es/elements/ElementLookup.d.ts +20 -0
  48. package/es/elements/ElementLookup.js +30 -0
  49. package/es/elements/ElementLookupDialog/BarcodeScanner/DeadIcon.d.ts +2 -0
  50. package/es/elements/ElementLookupDialog/BarcodeScanner/DeadIcon.js +5 -0
  51. package/es/elements/ElementLookupDialog/BarcodeScanner/Error.d.ts +4 -0
  52. package/es/elements/ElementLookupDialog/BarcodeScanner/Error.js +33 -0
  53. package/es/elements/ElementLookupDialog/BarcodeScanner/index.d.ts +10 -0
  54. package/es/elements/ElementLookupDialog/BarcodeScanner/index.js +37 -0
  55. package/es/elements/ElementLookupDialog/BarcodeScanner/useScanner.d.ts +16 -0
  56. package/es/elements/ElementLookupDialog/BarcodeScanner/useScanner.js +135 -0
  57. package/es/elements/ElementLookupDialog/HIDScanner/index.css +17 -0
  58. package/es/elements/ElementLookupDialog/HIDScanner/index.d.ts +4 -0
  59. package/es/elements/ElementLookupDialog/HIDScanner/index.js +36 -0
  60. package/es/elements/ElementLookupDialog/HIDScanner/useHIDScanner.d.ts +1 -0
  61. package/es/elements/ElementLookupDialog/HIDScanner/useHIDScanner.js +27 -0
  62. package/es/elements/ElementLookupDialog/Manual/ManualForm.d.ts +12 -0
  63. package/es/elements/ElementLookupDialog/Manual/ManualForm.js +48 -0
  64. package/es/elements/ElementLookupDialog/getElement.d.ts +23 -0
  65. package/es/elements/ElementLookupDialog/getElement.js +55 -0
  66. package/es/elements/ElementLookupDialog/index.d.ts +24 -0
  67. package/es/elements/ElementLookupDialog/index.js +97 -0
  68. package/es/elements/ElementLookupDialog/utils/Giai.d.ts +52 -0
  69. package/es/elements/ElementLookupDialog/utils/Giai.js +158 -0
  70. package/es/elements/ElementLookupDialog/utils/beep.d.ts +10 -0
  71. package/es/elements/ElementLookupDialog/utils/beep.js +20 -0
  72. package/es/elements/ProjectSelect.d.ts +23 -0
  73. package/es/elements/ProjectSelect.js +42 -0
  74. package/es/elements/data/dsAttachments.d.ts +15 -0
  75. package/es/elements/data/dsAttachments.js +85 -0
  76. package/es/elements/data/dsCurrentAttachment.d.ts +13 -0
  77. package/es/elements/data/dsCurrentAttachment.js +75 -0
  78. package/es/elements/data/dsDamages.d.ts +25 -0
  79. package/es/elements/data/dsDamages.js +127 -0
  80. package/es/elements/data/dsDamagesDuplicateCheck.d.ts +2 -0
  81. package/es/elements/data/dsDamagesDuplicateCheck.js +3 -0
  82. package/es/elements/data/dsElements.d.ts +8 -0
  83. package/es/elements/data/dsElements.js +47 -0
  84. package/es/elements/data/dsProjects.d.ts +8 -0
  85. package/es/elements/data/dsProjects.js +51 -0
  86. package/es/elements/data/dsReportProjects.d.ts +2 -0
  87. package/es/elements/data/dsReportProjects.js +3 -0
  88. package/es/elements/data/dsTransportElements.d.ts +10 -0
  89. package/es/elements/data/dsTransportElements.js +57 -0
  90. package/es/elements/data/index.d.ts +11 -0
  91. package/es/elements/data/index.js +11 -0
  92. package/es/elements/data/procApproveDamage.d.ts +2 -0
  93. package/es/elements/data/procApproveDamage.js +18 -0
  94. package/es/elements/data/procDeleteAttachment.d.ts +2 -0
  95. package/es/elements/data/procDeleteAttachment.js +18 -0
  96. package/es/elements/data/procDeleteDamage.d.ts +2 -0
  97. package/es/elements/data/procDeleteDamage.js +18 -0
  98. package/es/elements/data/procRemoveDamageFix.d.ts +2 -0
  99. package/es/elements/data/procRemoveDamageFix.js +18 -0
  100. package/es/elements/data/procRestoreAttachments.d.ts +2 -0
  101. package/es/elements/data/procRestoreAttachments.js +18 -0
  102. package/es/elements/index.d.ts +9 -0
  103. package/es/elements/index.js +9 -0
  104. package/es/index.css +1 -1
  105. package/es/index.d.ts +12 -11
  106. package/es/index.js +12 -11
  107. package/package.json +16 -40
  108. package/src/{Checkbox → components/Checkbox}/index.tsx +1 -1
  109. package/{es → src/components}/ColorCard/index.css +10 -6
  110. package/src/{DateNavigator → components/DateNavigator}/index.tsx +2 -0
  111. package/src/components/FilePond.tsx +312 -0
  112. package/src/{LinkedCardList → components/LinkedCardList}/index.scss +1 -1
  113. package/src/{PageBanner → components/PageBanner}/index.scss +1 -1
  114. package/src/{PageBanner → components/PageBanner}/index.tsx +0 -1
  115. package/src/{ProgressBar → components/ProgressBar}/index.scss +1 -1
  116. package/src/{Sidebar → components/Sidebar}/index.tsx +3 -2
  117. package/src/{SplitContainer → components/SplitContainer}/index.tsx +1 -1
  118. package/src/{ValueToggleGroup → components/ValueToggleGroup}/index.css +18 -16
  119. package/src/{ValueToggleGroup → components/ValueToggleGroup}/index.tsx +2 -1
  120. package/src/elements/ElementDamage/DamageFormButtons.tsx +35 -0
  121. package/src/elements/ElementDamage/DescriptionInput.tsx +111 -0
  122. package/src/elements/ElementDamage/DuplicateDamage/DesktopView.tsx +75 -0
  123. package/src/elements/ElementDamage/DuplicateDamage/DublicateElementDamageDialog.tsx +65 -0
  124. package/src/elements/ElementDamage/DuplicateDamage/PhoneView.tsx +42 -0
  125. package/src/elements/ElementDamage/DuplicateDamage/index.tsx +105 -0
  126. package/src/elements/ElementDamage/ElementNumberTextField.tsx +41 -0
  127. package/src/elements/ElementDamage/HiddenDamageFields.tsx +13 -0
  128. package/src/elements/ElementDamage/RegisterFix.tsx +100 -0
  129. package/src/elements/ElementDamage/SubmitButtons.tsx +40 -0
  130. package/src/elements/ElementDamage/TransportElementSelect.tsx +52 -0
  131. package/src/elements/ElementDamage/index.css +3 -0
  132. package/src/elements/ElementLookup.tsx +90 -0
  133. package/src/elements/ElementLookupDialog/BarcodeScanner/DeadIcon.tsx +12 -0
  134. package/src/elements/ElementLookupDialog/BarcodeScanner/Error.tsx +46 -0
  135. package/src/elements/ElementLookupDialog/BarcodeScanner/index.tsx +75 -0
  136. package/src/elements/ElementLookupDialog/BarcodeScanner/useScanner.ts +170 -0
  137. package/src/elements/ElementLookupDialog/HIDScanner/index.css +17 -0
  138. package/src/elements/ElementLookupDialog/HIDScanner/index.tsx +67 -0
  139. package/src/elements/ElementLookupDialog/HIDScanner/useHIDScanner.ts +30 -0
  140. package/src/elements/ElementLookupDialog/Manual/ManualForm.tsx +110 -0
  141. package/src/elements/ElementLookupDialog/getElement.ts +89 -0
  142. package/src/elements/ElementLookupDialog/index.tsx +211 -0
  143. package/src/elements/ElementLookupDialog/utils/Giai.ts +209 -0
  144. package/src/elements/ElementLookupDialog/utils/beep.ts +35 -0
  145. package/src/elements/ProjectSelect.tsx +128 -0
  146. package/src/elements/data/dsAttachments.ts +101 -0
  147. package/src/elements/data/dsCurrentAttachment.ts +89 -0
  148. package/src/elements/data/dsDamages.ts +166 -0
  149. package/src/elements/data/dsDamagesDuplicateCheck.ts +4 -0
  150. package/src/elements/data/dsElements.ts +56 -0
  151. package/src/elements/data/dsProjects.ts +60 -0
  152. package/src/elements/data/dsReportProjects.ts +4 -0
  153. package/src/elements/data/dsTransportElements.ts +68 -0
  154. package/src/elements/data/index.ts +11 -0
  155. package/src/elements/data/procApproveDamage.ts +19 -0
  156. package/src/elements/data/procDeleteAttachment.ts +19 -0
  157. package/src/elements/data/procDeleteDamage.ts +19 -0
  158. package/src/elements/data/procRemoveDamageFix.ts +19 -0
  159. package/src/elements/data/procRestoreAttachments.ts +19 -0
  160. package/src/elements/index.ts +9 -0
  161. package/src/index.scss +10 -10
  162. package/src/index.ts +13 -13
  163. /package/es/{Checkbox → components/Checkbox}/index.css +0 -0
  164. /package/es/{Checkbox → components/Checkbox}/index.d.ts +0 -0
  165. /package/es/{Checkbox → components/Checkbox}/index.js +0 -0
  166. /package/es/{ColorCard → components/ColorCard}/index.d.ts +0 -0
  167. /package/es/{ColorCard → components/ColorCard}/index.js +0 -0
  168. /package/es/{DateNavigator → components/DateNavigator}/index.css +0 -0
  169. /package/es/{DateNavigator → components/DateNavigator}/index.d.ts +0 -0
  170. /package/es/{LinkedCardList → components/LinkedCardList}/index.css +0 -0
  171. /package/es/{LinkedCardList → components/LinkedCardList}/index.d.ts +0 -0
  172. /package/es/{LinkedCardList → components/LinkedCardList}/index.js +0 -0
  173. /package/es/{PageBanner → components/PageBanner}/index.css +0 -0
  174. /package/es/{PageBanner → components/PageBanner}/index.d.ts +0 -0
  175. /package/es/{Portal → components/Portal}/index.d.ts +0 -0
  176. /package/es/{Portal → components/Portal}/index.js +0 -0
  177. /package/es/{ProgressBar → components/ProgressBar}/index.css +0 -0
  178. /package/es/{ProgressBar → components/ProgressBar}/index.d.ts +0 -0
  179. /package/es/{ProgressBar → components/ProgressBar}/index.js +0 -0
  180. /package/es/{Sidebar → components/Sidebar}/index.css +0 -0
  181. /package/es/{Sidebar → components/Sidebar}/index.js +0 -0
  182. /package/es/{Spinner → components/Spinner}/index.css +0 -0
  183. /package/es/{Spinner → components/Spinner}/index.d.ts +0 -0
  184. /package/es/{Spinner → components/Spinner}/index.js +0 -0
  185. /package/es/{SplitContainer → components/SplitContainer}/index.css +0 -0
  186. /package/es/{SplitContainer → components/SplitContainer}/index.d.ts +0 -0
  187. /package/es/{ValueToggleGroup → components/ValueToggleGroup}/index.d.ts +0 -0
  188. /package/src/{Checkbox → components/Checkbox}/index.scss +0 -0
  189. /package/src/{ColorCard → components/ColorCard}/index.tsx +0 -0
  190. /package/src/{DateNavigator → components/DateNavigator}/index.css +0 -0
  191. /package/src/{LinkedCardList → components/LinkedCardList}/index.tsx +0 -0
  192. /package/src/{Portal → components/Portal}/index.tsx +0 -0
  193. /package/src/{ProgressBar → components/ProgressBar}/index.tsx +0 -0
  194. /package/src/{Sidebar → components/Sidebar}/index.scss +0 -0
  195. /package/src/{Spinner → components/Spinner}/index.css +0 -0
  196. /package/src/{Spinner → components/Spinner}/index.tsx +0 -0
  197. /package/src/{SplitContainer → components/SplitContainer}/index.scss +0 -0
@@ -0,0 +1,110 @@
1
+ import Add from "@mui/icons-material/Add";
2
+ import { Alert, Button, TextField } from "@mui/material";
3
+ import { getLocalizedString } from "@olenbetong/appframe-core";
4
+ import { useId, useRef, useState } from "react";
5
+ import type { ProjectsRecord } from "../../data/dsProjects";
6
+ import { ProjectSelect } from "../../ProjectSelect";
7
+ import { type ElementDetails, getElement } from "../getElement";
8
+
9
+ const isElementsOnly = /^([a-z]{2,4}[\s]*[0-9]{4,8}[\s,]*)+$/i;
10
+ const matchElements = /([a-z]{2,4}[\s]*[0-9]{4,8})/gi;
11
+
12
+ export type ProjectRecordBase = {
13
+ Domain: string;
14
+ ProjectID: string;
15
+ ProjectName?: string;
16
+ };
17
+
18
+ export type AddElementManualFormProps = {
19
+ onAdd: (element: string | ElementDetails) => Promise<void>;
20
+ mode?: "single" | "multi";
21
+ defaultProject?: ProjectRecordBase;
22
+ };
23
+
24
+ export function AddElementManualForm({ onAdd, mode, defaultProject }: AddElementManualFormProps) {
25
+ let id = useId();
26
+ let buttonRef = useRef<HTMLButtonElement>(null);
27
+ let [working, setWorking] = useState(false);
28
+ let [elementId, setElementId] = useState<string>("");
29
+ let [project, setProject] = useState<Partial<ProjectsRecord> | null>(defaultProject ?? null);
30
+
31
+ async function handleSubmit(evt: React.FormEvent<HTMLFormElement>) {
32
+ try {
33
+ evt.preventDefault();
34
+ evt.stopPropagation();
35
+ let formData = new FormData(evt.target as HTMLFormElement);
36
+ let elements = formData.get("ElementID");
37
+ let domain = formData.get("Domain");
38
+ let projectId = formData.get("ProjectID");
39
+
40
+ if (!projectId || !domain || !elements) {
41
+ let warning = getLocalizedString(
42
+ mode === "multi" ? "Project and elements are required" : "Project and element are required",
43
+ );
44
+ alert(warning);
45
+ return;
46
+ }
47
+
48
+ setWorking(true);
49
+ let record = (elements as string)
50
+ .match(matchElements)
51
+ ?.map((elementId) => elementId.replace(/\s+/g, ""))
52
+ .map((elementId) => getElement({ projectId: projectId as string, elementId: elementId }, { mode: "manual" }));
53
+
54
+ if (!record) {
55
+ alert(getLocalizedString("Invalid elements"));
56
+ return;
57
+ }
58
+
59
+ let loaded = (await Promise.all(record)).map((record) => onAdd(record));
60
+ await Promise.all(loaded);
61
+ setWorking(false);
62
+ } catch (error) {
63
+ setWorking(false);
64
+ alert((error as Error).message);
65
+ }
66
+ }
67
+
68
+ return (
69
+ <form id={id} onSubmit={handleSubmit} method="post">
70
+ <Alert severity="warning" sx={{ mb: 2 }}>
71
+ {getLocalizedString(
72
+ "Manually entering elements is a major source of errors. Only use this as a last resort. If the camera or scanner is not working, please contact the IT department.",
73
+ )}
74
+ </Alert>
75
+
76
+ <ProjectSelect
77
+ onChange={(project) => (project ? setProject(project) : setProject(null))}
78
+ value={project}
79
+ id="AddElement_Project"
80
+ />
81
+
82
+ <TextField
83
+ variant="filled"
84
+ fullWidth
85
+ autoComplete="off"
86
+ margin="normal"
87
+ name="ElementID"
88
+ id="AddElement_ElementID"
89
+ label={getLocalizedString("Element ID")}
90
+ value={elementId}
91
+ onChange={(evt) => setElementId(evt.target.value)}
92
+ />
93
+
94
+ <Button
95
+ form={id}
96
+ ref={buttonRef}
97
+ color="primary"
98
+ variant="contained"
99
+ size="large"
100
+ type="submit"
101
+ disabled={!isElementsOnly.test(elementId) || working}
102
+ loading={working}
103
+ loadingPosition="end"
104
+ endIcon={<Add />}
105
+ >
106
+ {getLocalizedString("Add")}
107
+ </Button>
108
+ </form>
109
+ );
110
+ }
@@ -0,0 +1,89 @@
1
+ import { logActivity, startActivity } from "@olenbetong/appframe-core";
2
+ import { DataProviderHandlerAPI } from "@olenbetong/appframe-data";
3
+ import Giai from "./utils/Giai";
4
+
5
+ export type AddElementSource = "manual" | "camera-barcode" | "loading-list" | "hid-barcode" | "hid-rfid";
6
+
7
+ export type ElementDetails = {
8
+ Domain: string;
9
+ PrimKey: string;
10
+ Created: Date;
11
+ ProjectID: string;
12
+ ProjectName: string;
13
+ ProjectDomain: string;
14
+ ElementID: string;
15
+ DisplayName: string;
16
+ GiaiUri: string;
17
+ };
18
+
19
+ const elementsDataHandler = new DataProviderHandlerAPI<ElementDetails>({
20
+ dataSourceId: "aviw_Fleet_DeliveryTickets_ElementsLookup",
21
+ fields: [
22
+ { name: "PrimKey", type: "string" },
23
+ { name: "Created", type: "datetime" },
24
+ { name: "Domain", type: "string" },
25
+ { name: "ProjectID", type: "string" },
26
+ { name: "ProjectName", type: "string" },
27
+ { name: "ProjectDomain", type: "string" },
28
+ { name: "ElementID", type: "string" },
29
+ { name: "DisplayName", type: "string" },
30
+ { name: "GiaiUri", type: "string" },
31
+ ],
32
+ });
33
+
34
+ const elementDetailsCache = new Map<string, ElementDetails>();
35
+
36
+ export type ElementFilter = {
37
+ projectId: string;
38
+ elementId: string;
39
+ };
40
+
41
+ export type GetElementContext = {
42
+ filter?: string | ElementFilter;
43
+ mode?: AddElementSource;
44
+ method?: "cache" | "database";
45
+ result?: null | ElementDetails;
46
+ };
47
+
48
+ export async function getElement(id: string | ElementFilter, context: GetElementContext): Promise<ElementDetails> {
49
+ let details: ElementDetails;
50
+ context.filter = id;
51
+
52
+ if (typeof id === "string" && elementDetailsCache.has(id)) {
53
+ details = elementDetailsCache.get(id) as ElementDetails;
54
+ context.method = "cache";
55
+ context.result = details;
56
+ logActivity("GetElement", context);
57
+ } else {
58
+ context.method = "database";
59
+
60
+ let logActivity = startActivity("GetElement", context);
61
+ let whereClause: string;
62
+ if (typeof id === "string") {
63
+ let uri = Giai.fromString(id);
64
+ whereClause = `[GiaiUri] = '${uri.toEpcPureIdentityUri()}'`;
65
+ } else {
66
+ whereClause = `[ProjectID] = '${id.projectId}' AND [ElementID] = '${id.elementId}'`;
67
+ }
68
+
69
+ let result = await elementsDataHandler.retrieve<ElementDetails>({
70
+ whereClause,
71
+ });
72
+
73
+ if (!result.length) {
74
+ context.result = null;
75
+ logActivity();
76
+ throw Error(`Element not found`);
77
+ }
78
+
79
+ details = result[0];
80
+ context.result = details;
81
+ logActivity();
82
+
83
+ if (typeof id === "string") {
84
+ elementDetailsCache.set(id, details);
85
+ }
86
+ }
87
+
88
+ return details;
89
+ }
@@ -0,0 +1,211 @@
1
+ import Add from "@mui/icons-material/Add";
2
+ import DeleteForeverOutlined from "@mui/icons-material/DeleteForeverOutlined";
3
+ import Edit from "@mui/icons-material/Edit";
4
+ import QrCodeScanner from "@mui/icons-material/QrCodeScanner";
5
+ import TapAndPlay from "@mui/icons-material/TapAndPlay";
6
+ import { Alert, Box, Button, DialogActions, List, ListItem, ListItemText, Tab, Tabs } from "@mui/material";
7
+ import { getLocalizedString, localize } from "@olenbetong/appframe-core";
8
+ import { ResponsiveDialog } from "@olenbetong/appframe-mui";
9
+ import { useEffect, useState } from "react";
10
+ import { flushSync } from "react-dom";
11
+ import { BarcodeScanner } from "./BarcodeScanner";
12
+ import { type AddElementSource, type ElementDetails, getElement } from "./getElement";
13
+ import { AddElementHIDScanner } from "./HIDScanner";
14
+ import { AddElementManualForm } from "./Manual/ManualForm";
15
+ import { useBeeper } from "./utils/beep";
16
+ import Giai from "./utils/Giai";
17
+
18
+ export type AddElement = {
19
+ element: ElementDetails;
20
+ mode: AddElementSource;
21
+ scanTime?: number;
22
+ };
23
+
24
+ type InitialTabProps = "camera" | "scanner" | "manual";
25
+
26
+ export type ElementLookupDialogSharedProps = {
27
+ open: boolean;
28
+ onClose: () => void;
29
+ initialTab?: InitialTabProps | null;
30
+ useSearch?: boolean;
31
+ };
32
+
33
+ export type ElementLookupDialogSingleModeProps = ElementLookupDialogSharedProps & {
34
+ mode: "single" | "search";
35
+ onAdd: (element: AddElement) => Promise<void>;
36
+ };
37
+
38
+ export type ElementLookupDialogMultiModeProps = ElementLookupDialogSharedProps & {
39
+ mode: "multi";
40
+ onAdd: (element: AddElement[]) => Promise<void>;
41
+ };
42
+
43
+ export type ElementLookupDialogProps = ElementLookupDialogSingleModeProps | ElementLookupDialogMultiModeProps;
44
+
45
+ export function ElementLookupDialog(props: ElementLookupDialogProps) {
46
+ let { open, onClose, initialTab } = props;
47
+ let [tab, setTab] = useState<"camera" | "scanner" | "manual">("camera");
48
+ let [elements, setElements] = useState<Array<AddElement>>([]);
49
+ let [adding, setAdding] = useState(false);
50
+ let beeper = useBeeper();
51
+
52
+ useEffect(() => {
53
+ if (open && initialTab) {
54
+ setTab(initialTab);
55
+ }
56
+ }, [open, initialTab]);
57
+
58
+ async function handleElementAdd(element: string | ElementDetails, mode: AddElementSource) {
59
+ try {
60
+ let record = typeof element === "string" ? await getElement(element.replace("\u001d", ""), { mode }) : element;
61
+
62
+ // Flush changes to DOM to be able to query newly added elements and animate their background
63
+ flushSync(() => {
64
+ setElements((current) => {
65
+ let now = Date.now();
66
+ let exists = current.some((e) => e.element.ElementID === record.ElementID);
67
+ let updated: Array<AddElement>;
68
+
69
+ if (exists) {
70
+ updated = current.map((e) => (e.element.ElementID === record.ElementID ? { ...e, scanTime: now } : e));
71
+ } else {
72
+ beeper();
73
+ updated =
74
+ props.mode === "multi"
75
+ ? [{ element: record, mode, scanTime: now }, ...current]
76
+ : [{ element: record, mode, scanTime: now }];
77
+ }
78
+
79
+ if (props.mode === "multi") {
80
+ updated.sort((a, b) => (b.scanTime ?? 0) - (a.scanTime ?? 0));
81
+ }
82
+
83
+ return updated;
84
+ });
85
+ });
86
+
87
+ let addedDomElement = document.querySelector(`[data-elementid="${record.ElementID}"]`);
88
+ if (addedDomElement) {
89
+ addedDomElement.animate(
90
+ [
91
+ { backgroundColor: "var(--brand-light)", offset: 0 },
92
+ { backgroundColor: "var(--brand-light)", offset: 0.2 },
93
+ { backgroundColor: "transparent" },
94
+ ],
95
+ {
96
+ duration: 1000,
97
+ easing: "ease-out",
98
+ },
99
+ );
100
+ }
101
+ } catch (error) {
102
+ alert((error as Error).message);
103
+ }
104
+ }
105
+
106
+ if (tab === "manual" && props.mode === "search") {
107
+ tab = "camera";
108
+ }
109
+
110
+ return (
111
+ <ResponsiveDialog
112
+ open={open}
113
+ onClose={onClose}
114
+ slotProps={{
115
+ paper: { sx: { overflow: "hidden" } },
116
+ }}
117
+ >
118
+ <Box display="flex" flexDirection="column" maxHeight="100%" overflow="hidden" flexGrow={1}>
119
+ <Tabs value={tab} onChange={(_, newTab) => setTab(newTab)} variant="fullWidth" sx={{ flexShrink: 0 }}>
120
+ {props.mode !== "search" && <Tab icon={<Edit />} label={getLocalizedString("Manual")} value="manual" />}
121
+ <Tab icon={<TapAndPlay />} label={getLocalizedString("Scanner")} value="scanner" />
122
+ <Tab icon={<QrCodeScanner />} label={getLocalizedString("Camera")} value="camera" />
123
+ </Tabs>
124
+ {tab === "camera" && (
125
+ <Box flexBasis="12rem" flexGrow={1} flexShrink={2}>
126
+ <BarcodeScanner onRead={(element) => handleElementAdd(element, "camera-barcode")} />
127
+ </Box>
128
+ )}
129
+ {tab === "manual" && (
130
+ <Box p={2}>
131
+ <AddElementManualForm onAdd={(element) => handleElementAdd(element, "manual")} />
132
+ </Box>
133
+ )}
134
+ {tab === "scanner" && (
135
+ <Box flexBasis="12rem" flexGrow={1} flexShrink={2}>
136
+ <AddElementHIDScanner
137
+ onScan={(element) =>
138
+ handleElementAdd(element, element.startsWith(Giai.BARCODE_PREFIX) ? "hid-barcode" : "hid-rfid")
139
+ }
140
+ />
141
+ </Box>
142
+ )}
143
+
144
+ <Box flex="3 1 14rem" overflow="auto">
145
+ {props.mode === "multi" && (
146
+ <Alert
147
+ icon={false}
148
+ severity="info"
149
+ sx={{ justifyContent: "center", py: 0, position: "sticky", top: 0, zIndex: 1 }}
150
+ slotProps={{
151
+ message: { sx: { py: 0.5 } },
152
+ }}
153
+ >{localize`${elements.length} elements selected`}</Alert>
154
+ )}
155
+ <List disablePadding>
156
+ {elements.map(({ element }) => (
157
+ <ListItem
158
+ key={element.ElementID}
159
+ data-elementid={element.ElementID}
160
+ secondaryAction={
161
+ <Button
162
+ color="error"
163
+ variant="outlined"
164
+ onClick={() => {
165
+ setElements((current) =>
166
+ props.mode === "multi" ? current.filter((e) => e.element.ElementID !== element.ElementID) : [],
167
+ );
168
+ }}
169
+ >
170
+ <DeleteForeverOutlined />
171
+ </Button>
172
+ }
173
+ >
174
+ <ListItemText primary={element.DisplayName} secondary={element.ElementID} />
175
+ </ListItem>
176
+ ))}
177
+ </List>
178
+ </Box>
179
+
180
+ <DialogActions sx={{ flexShrink: 0 }}>
181
+ <Button onClick={onClose}>{getLocalizedString("Cancel")}</Button>
182
+ <Button
183
+ variant="contained"
184
+ loading={adding}
185
+ onClick={async () => {
186
+ try {
187
+ setAdding(true);
188
+ if (props.mode === "multi") {
189
+ await props.onAdd(elements);
190
+ } else {
191
+ await props.onAdd(elements[0]!);
192
+ }
193
+ setElements([]);
194
+ setAdding(false);
195
+ onClose();
196
+ } catch (error) {
197
+ setAdding(false);
198
+ alert((error as Error).message);
199
+ }
200
+ }}
201
+ startIcon={<Add />}
202
+ loadingPosition="start"
203
+ disabled={!elements.length}
204
+ >
205
+ {props.mode === "search" ? getLocalizedString("Search for element") : getLocalizedString("Add element")}
206
+ </Button>
207
+ </DialogActions>
208
+ </Box>
209
+ </ResponsiveDialog>
210
+ );
211
+ }
@@ -0,0 +1,209 @@
1
+ import { getLocalizedString } from "@olenbetong/appframe-core";
2
+
3
+ export type GiaiProps = {
4
+ assetReference: string | number | bigint;
5
+ companyPrefix: string | number | bigint;
6
+ filter?: string | number | undefined;
7
+ };
8
+
9
+ /**
10
+ * GIAI-96 encoder and decoder as defined by the GS1 EPC Tag data standard
11
+ */
12
+ export default class Giai {
13
+ /// Bit lengths for company prefix given partition equal to index.
14
+ ///
15
+ /// Partition Company prefix Individual asset reference
16
+ /// Bits Digits Bits Digits
17
+ /// 0 40 12 42 13
18
+ /// 1 37 11 45 14
19
+ /// 2 34 10 48 15
20
+ /// 3 30 9 52 16
21
+ /// 4 27 8 55 17
22
+ /// 5 24 7 58 18
23
+ /// 6 20 6 62 19
24
+ static PARTITION_TABLE = [40, 37, 34, 30, 27, 24, 20];
25
+ // EPC header for GIAI-96 coding scheme
26
+ static EPC_HEADER = "00110100";
27
+ // Prefix for GS1 key used in barcodes;
28
+ static BARCODE_PREFIX = "8004";
29
+ // Group separator used to separate company prefix and asset reference (FNC1 in GS1-128 barcodes)
30
+ static GROUP_SEPARATOR = String.fromCodePoint(0x001d);
31
+
32
+ static URN_PREFIX = "urn:epc:id:giai:";
33
+
34
+ static fromHex = (hexIdentifier: string) => {
35
+ const bitIdentifier = hexToBitString(hexIdentifier);
36
+
37
+ if (bitIdentifier.substring(0, 8) !== Giai.EPC_HEADER) {
38
+ throw new Error("EPC value is not a valid GIAI value.");
39
+ }
40
+
41
+ const partition = Number.parseInt(bitIdentifier.substr(11, 3), 2);
42
+ const companyPrefixLength = Giai.PARTITION_TABLE[partition];
43
+ const filter = Number.parseInt(bitIdentifier.substring(8, 11), 2);
44
+ const companyPrefix = BigInt(`0b${bitIdentifier.substring(14, 14 + companyPrefixLength)}`);
45
+ const assetReference = BigInt(`0b${bitIdentifier.substring(14 + companyPrefixLength)}`);
46
+
47
+ return new Giai({ assetReference, companyPrefix, filter });
48
+ };
49
+
50
+ /**
51
+ *
52
+ * @param {string | null | undefined} barcodeString
53
+ * @returns
54
+ */
55
+ static fromBarcode = (barcodeString: string) => {
56
+ const prefixLength = Giai.BARCODE_PREFIX.length;
57
+
58
+ if (barcodeString[0] === Giai.GROUP_SEPARATOR) {
59
+ barcodeString = barcodeString.substr(1);
60
+ }
61
+
62
+ if (barcodeString.substr(0, prefixLength) !== Giai.BARCODE_PREFIX) {
63
+ throw new Error(`Barcode is not a valid GIAI barcode. (${Giai.BARCODE_PREFIX} prefix expected)`);
64
+ }
65
+
66
+ let giaiString = barcodeString.substring(prefixLength);
67
+ const gsIndex = giaiString.indexOf(Giai.GROUP_SEPARATOR);
68
+
69
+ if (gsIndex >= 0) {
70
+ return new Giai({
71
+ assetReference: giaiString.substring(gsIndex + 1),
72
+ companyPrefix: giaiString.substring(0, gsIndex),
73
+ });
74
+ }
75
+
76
+ // If no separator is found, assume a company prefix length of 7
77
+ return new Giai({
78
+ assetReference: giaiString.substring(7),
79
+ companyPrefix: giaiString.substring(0, 7),
80
+ });
81
+ };
82
+
83
+ /**
84
+ * Creates a GIAI instance from a purse identity URI (urn:epc:giai:[companyprefix].[assetreference])
85
+ * @param {string} uri EPC GIAI pure identity URI
86
+ */
87
+ static fromEpcPureIdentityUri(uri: string) {
88
+ if (!uri.startsWith(Giai.URN_PREFIX)) {
89
+ throw Error("URI is not a valid GIAI URI");
90
+ }
91
+
92
+ let [companyPrefix, assetReference] = uri.substring(Giai.URN_PREFIX.length).split(".");
93
+
94
+ return new Giai({
95
+ assetReference,
96
+ companyPrefix,
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Attempt to create a GIAI instance from a string with unknown format. Could be either a URN, barcode value
102
+ * or EPC value.
103
+ * @param {string} value
104
+ * @returns
105
+ */
106
+ static fromString(value: string, ignoreError?: false): Giai;
107
+ static fromString(value: string, ignoreError: true): Giai | undefined;
108
+ static fromString(value: string, ignoreError = false) {
109
+ try {
110
+ if (value.startsWith("0x")) {
111
+ value = value.replace("0x", "");
112
+ return Giai.fromHex(value.substring(0, 24));
113
+ // return Giai.fromHex(value.substring(6, value.length - 4));
114
+ } else if (value.startsWith(Giai.URN_PREFIX)) {
115
+ return Giai.fromEpcPureIdentityUri(value);
116
+ } else if (value.startsWith(Giai.BARCODE_PREFIX)) {
117
+ return Giai.fromBarcode(value);
118
+ } else if (value.length === 24) {
119
+ return Giai.fromHex(value);
120
+ }
121
+ } catch (ex) {
122
+ if (!ignoreError) {
123
+ throw ex;
124
+ }
125
+ }
126
+
127
+ if (!ignoreError) {
128
+ throw Error(
129
+ getLocalizedString(
130
+ "Can't create GIAI from string. Failed to match value '{value}' with known input formats.",
131
+ ).replace("{value}", value),
132
+ );
133
+ }
134
+
135
+ return undefined;
136
+ }
137
+
138
+ /**
139
+ * @type {string | number | bigint}
140
+ */
141
+ #assetReference;
142
+ /**
143
+ * @type {string | number | bigint}
144
+ */
145
+ #companyPrefix;
146
+
147
+ /**
148
+ *
149
+ * @param {GiaiProps} options
150
+ */
151
+ constructor({ assetReference, companyPrefix }: GiaiProps) {
152
+ this.#assetReference = assetReference;
153
+ this.#companyPrefix = companyPrefix;
154
+ }
155
+
156
+ get assetReference() {
157
+ return this.#assetReference;
158
+ }
159
+
160
+ get companyPrefix() {
161
+ return this.#companyPrefix;
162
+ }
163
+
164
+ // Returns the EPC pure identity uri for this identifier - as used by EPCIS.
165
+ toEpcPureIdentityUri() {
166
+ return `${Giai.URN_PREFIX}${this.companyPrefix}.${this.assetReference}`;
167
+ }
168
+
169
+ toGiaiBarcode() {
170
+ return Giai.BARCODE_PREFIX + this.companyPrefix + Giai.GROUP_SEPARATOR + this.assetReference;
171
+ }
172
+
173
+ toEpcHex() {
174
+ let bitIdentifier = Giai.EPC_HEADER;
175
+ bitIdentifier += "000"; // Filter
176
+ bitIdentifier += (5).toString(2).padStart(3, "0"); // Partition
177
+ let companyPrefix = ["bigint", "number"].includes(typeof this.#companyPrefix)
178
+ ? this.#companyPrefix
179
+ : BigInt(this.#companyPrefix);
180
+ let assetReference = ["bigint", "number"].includes(typeof this.#assetReference)
181
+ ? this.#assetReference
182
+ : BigInt(this.#assetReference);
183
+ bitIdentifier += companyPrefix.toString(2).padStart(24, "0");
184
+ bitIdentifier += assetReference.toString(2).padStart(58, "0");
185
+
186
+ return BigInt(`0b${bitIdentifier}`).toString(16).toUpperCase();
187
+ }
188
+ }
189
+
190
+ export function hexToBitString(hexString: string) {
191
+ let bitString = "";
192
+
193
+ for (let i = 0; i < hexString.length; i += 2) {
194
+ const byteString = hexString.substr(i, 2);
195
+ const byteBits = Number.parseInt(byteString, 16).toString(2);
196
+
197
+ bitString += byteBits.padStart(8, "0");
198
+ }
199
+
200
+ return bitString;
201
+ }
202
+
203
+ declare global {
204
+ interface Window {
205
+ Giai: typeof Giai | undefined;
206
+ }
207
+ }
208
+
209
+ window.Giai = Giai;
@@ -0,0 +1,35 @@
1
+ import { useState } from "react";
2
+
3
+ export type Beeper = (callback?: () => void) => void;
4
+
5
+ declare global {
6
+ interface Window {
7
+ webkitAudioContext: { new (): AudioContext };
8
+ }
9
+ }
10
+
11
+ export function createBeeper(duration = 200, type: OscillatorType = "sine"): Beeper {
12
+ const ctxClass = window.AudioContext || window.webkitAudioContext;
13
+ const ctx = new ctxClass();
14
+ return (finishedCallback?: () => void) => {
15
+ duration = +duration;
16
+
17
+ const osc = ctx.createOscillator();
18
+
19
+ osc.type = type;
20
+
21
+ osc.connect(ctx.destination);
22
+ osc.start();
23
+
24
+ setTimeout(() => {
25
+ osc.stop();
26
+ finishedCallback?.();
27
+ }, duration);
28
+ };
29
+ }
30
+
31
+ export function useBeeper(duration = 200, type: OscillatorType = "sine"): Beeper {
32
+ const [beeper] = useState<Beeper>(() => createBeeper(duration, type));
33
+
34
+ return beeper;
35
+ }