@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.
- package/dist/esm/ob.react.css +1189 -27
- package/dist/esm/ob.react.js +45612 -708
- package/dist/esm/ob.react.min.css +24 -1
- package/dist/esm/ob.react.min.css.map +3 -3
- package/dist/esm/ob.react.min.js +253 -1
- package/dist/esm/ob.react.min.js.map +4 -4
- package/dist/iife/ob.react.css +1189 -27
- package/dist/iife/ob.react.js +45625 -720
- package/dist/iife/ob.react.min.css +24 -1
- package/dist/iife/ob.react.min.css.map +3 -3
- package/dist/iife/ob.react.min.js +253 -1
- package/dist/iife/ob.react.min.js.map +4 -4
- package/dist/styles/styles.css +1 -1
- package/dist/styles/styles.css.map +1 -1
- package/{src → es/components}/ColorCard/index.css +10 -6
- package/es/{DateNavigator → components/DateNavigator}/index.js +2 -0
- package/es/components/FilePond.d.ts +44 -0
- package/es/components/FilePond.js +205 -0
- package/es/{PageBanner → components/PageBanner}/index.js +0 -1
- package/es/{Sidebar → components/Sidebar}/index.d.ts +2 -2
- package/es/{SplitContainer → components/SplitContainer}/index.js +3 -3
- package/es/{ValueToggleGroup → components/ValueToggleGroup}/index.css +18 -16
- package/es/{ValueToggleGroup → components/ValueToggleGroup}/index.js +4 -2
- package/es/elements/ElementDamage/DamageFormButtons.d.ts +9 -0
- package/es/elements/ElementDamage/DamageFormButtons.js +7 -0
- package/es/elements/ElementDamage/DescriptionInput.d.ts +14 -0
- package/es/elements/ElementDamage/DescriptionInput.js +44 -0
- package/es/elements/ElementDamage/DuplicateDamage/DesktopView.d.ts +7 -0
- package/es/elements/ElementDamage/DuplicateDamage/DesktopView.js +16 -0
- package/es/elements/ElementDamage/DuplicateDamage/DublicateElementDamageDialog.d.ts +10 -0
- package/es/elements/ElementDamage/DuplicateDamage/DublicateElementDamageDialog.js +12 -0
- package/es/elements/ElementDamage/DuplicateDamage/PhoneView.d.ts +6 -0
- package/es/elements/ElementDamage/DuplicateDamage/PhoneView.js +8 -0
- package/es/elements/ElementDamage/DuplicateDamage/index.d.ts +36 -0
- package/es/elements/ElementDamage/DuplicateDamage/index.js +40 -0
- package/es/elements/ElementDamage/ElementNumberTextField.d.ts +10 -0
- package/es/elements/ElementDamage/ElementNumberTextField.js +18 -0
- package/es/elements/ElementDamage/HiddenDamageFields.d.ts +6 -0
- package/es/elements/ElementDamage/HiddenDamageFields.js +4 -0
- package/es/elements/ElementDamage/RegisterFix.d.ts +14 -0
- package/es/elements/ElementDamage/RegisterFix.js +45 -0
- package/es/elements/ElementDamage/SubmitButtons.d.ts +20 -0
- package/es/elements/ElementDamage/SubmitButtons.js +7 -0
- package/es/elements/ElementDamage/TransportElementSelect.d.ts +7 -0
- package/es/elements/ElementDamage/TransportElementSelect.js +16 -0
- package/es/elements/ElementDamage/index.css +3 -0
- package/es/elements/ElementLookup.d.ts +20 -0
- package/es/elements/ElementLookup.js +30 -0
- package/es/elements/ElementLookupDialog/BarcodeScanner/DeadIcon.d.ts +2 -0
- package/es/elements/ElementLookupDialog/BarcodeScanner/DeadIcon.js +5 -0
- package/es/elements/ElementLookupDialog/BarcodeScanner/Error.d.ts +4 -0
- package/es/elements/ElementLookupDialog/BarcodeScanner/Error.js +33 -0
- package/es/elements/ElementLookupDialog/BarcodeScanner/index.d.ts +10 -0
- package/es/elements/ElementLookupDialog/BarcodeScanner/index.js +37 -0
- package/es/elements/ElementLookupDialog/BarcodeScanner/useScanner.d.ts +16 -0
- package/es/elements/ElementLookupDialog/BarcodeScanner/useScanner.js +135 -0
- package/es/elements/ElementLookupDialog/HIDScanner/index.css +17 -0
- package/es/elements/ElementLookupDialog/HIDScanner/index.d.ts +4 -0
- package/es/elements/ElementLookupDialog/HIDScanner/index.js +36 -0
- package/es/elements/ElementLookupDialog/HIDScanner/useHIDScanner.d.ts +1 -0
- package/es/elements/ElementLookupDialog/HIDScanner/useHIDScanner.js +27 -0
- package/es/elements/ElementLookupDialog/Manual/ManualForm.d.ts +12 -0
- package/es/elements/ElementLookupDialog/Manual/ManualForm.js +48 -0
- package/es/elements/ElementLookupDialog/getElement.d.ts +23 -0
- package/es/elements/ElementLookupDialog/getElement.js +55 -0
- package/es/elements/ElementLookupDialog/index.d.ts +24 -0
- package/es/elements/ElementLookupDialog/index.js +97 -0
- package/es/elements/ElementLookupDialog/utils/Giai.d.ts +52 -0
- package/es/elements/ElementLookupDialog/utils/Giai.js +158 -0
- package/es/elements/ElementLookupDialog/utils/beep.d.ts +10 -0
- package/es/elements/ElementLookupDialog/utils/beep.js +20 -0
- package/es/elements/ProjectSelect.d.ts +23 -0
- package/es/elements/ProjectSelect.js +42 -0
- package/es/elements/data/dsAttachments.d.ts +15 -0
- package/es/elements/data/dsAttachments.js +85 -0
- package/es/elements/data/dsCurrentAttachment.d.ts +13 -0
- package/es/elements/data/dsCurrentAttachment.js +75 -0
- package/es/elements/data/dsDamages.d.ts +25 -0
- package/es/elements/data/dsDamages.js +127 -0
- package/es/elements/data/dsDamagesDuplicateCheck.d.ts +2 -0
- package/es/elements/data/dsDamagesDuplicateCheck.js +3 -0
- package/es/elements/data/dsElements.d.ts +8 -0
- package/es/elements/data/dsElements.js +47 -0
- package/es/elements/data/dsProjects.d.ts +8 -0
- package/es/elements/data/dsProjects.js +51 -0
- package/es/elements/data/dsReportProjects.d.ts +2 -0
- package/es/elements/data/dsReportProjects.js +3 -0
- package/es/elements/data/dsTransportElements.d.ts +10 -0
- package/es/elements/data/dsTransportElements.js +57 -0
- package/es/elements/data/index.d.ts +11 -0
- package/es/elements/data/index.js +11 -0
- package/es/elements/data/procApproveDamage.d.ts +2 -0
- package/es/elements/data/procApproveDamage.js +18 -0
- package/es/elements/data/procDeleteAttachment.d.ts +2 -0
- package/es/elements/data/procDeleteAttachment.js +18 -0
- package/es/elements/data/procDeleteDamage.d.ts +2 -0
- package/es/elements/data/procDeleteDamage.js +18 -0
- package/es/elements/data/procRemoveDamageFix.d.ts +2 -0
- package/es/elements/data/procRemoveDamageFix.js +18 -0
- package/es/elements/data/procRestoreAttachments.d.ts +2 -0
- package/es/elements/data/procRestoreAttachments.js +18 -0
- package/es/elements/index.d.ts +9 -0
- package/es/elements/index.js +9 -0
- package/es/index.css +1 -1
- package/es/index.d.ts +12 -11
- package/es/index.js +12 -11
- package/package.json +16 -40
- package/src/{Checkbox → components/Checkbox}/index.tsx +1 -1
- package/{es → src/components}/ColorCard/index.css +10 -6
- package/src/{DateNavigator → components/DateNavigator}/index.tsx +2 -0
- package/src/components/FilePond.tsx +312 -0
- package/src/{LinkedCardList → components/LinkedCardList}/index.scss +1 -1
- package/src/{PageBanner → components/PageBanner}/index.scss +1 -1
- package/src/{PageBanner → components/PageBanner}/index.tsx +0 -1
- package/src/{ProgressBar → components/ProgressBar}/index.scss +1 -1
- package/src/{Sidebar → components/Sidebar}/index.tsx +3 -2
- package/src/{SplitContainer → components/SplitContainer}/index.tsx +1 -1
- package/src/{ValueToggleGroup → components/ValueToggleGroup}/index.css +18 -16
- package/src/{ValueToggleGroup → components/ValueToggleGroup}/index.tsx +2 -1
- package/src/elements/ElementDamage/DamageFormButtons.tsx +35 -0
- package/src/elements/ElementDamage/DescriptionInput.tsx +111 -0
- package/src/elements/ElementDamage/DuplicateDamage/DesktopView.tsx +75 -0
- package/src/elements/ElementDamage/DuplicateDamage/DublicateElementDamageDialog.tsx +65 -0
- package/src/elements/ElementDamage/DuplicateDamage/PhoneView.tsx +42 -0
- package/src/elements/ElementDamage/DuplicateDamage/index.tsx +105 -0
- package/src/elements/ElementDamage/ElementNumberTextField.tsx +41 -0
- package/src/elements/ElementDamage/HiddenDamageFields.tsx +13 -0
- package/src/elements/ElementDamage/RegisterFix.tsx +100 -0
- package/src/elements/ElementDamage/SubmitButtons.tsx +40 -0
- package/src/elements/ElementDamage/TransportElementSelect.tsx +52 -0
- package/src/elements/ElementDamage/index.css +3 -0
- package/src/elements/ElementLookup.tsx +90 -0
- package/src/elements/ElementLookupDialog/BarcodeScanner/DeadIcon.tsx +12 -0
- package/src/elements/ElementLookupDialog/BarcodeScanner/Error.tsx +46 -0
- package/src/elements/ElementLookupDialog/BarcodeScanner/index.tsx +75 -0
- package/src/elements/ElementLookupDialog/BarcodeScanner/useScanner.ts +170 -0
- package/src/elements/ElementLookupDialog/HIDScanner/index.css +17 -0
- package/src/elements/ElementLookupDialog/HIDScanner/index.tsx +67 -0
- package/src/elements/ElementLookupDialog/HIDScanner/useHIDScanner.ts +30 -0
- package/src/elements/ElementLookupDialog/Manual/ManualForm.tsx +110 -0
- package/src/elements/ElementLookupDialog/getElement.ts +89 -0
- package/src/elements/ElementLookupDialog/index.tsx +211 -0
- package/src/elements/ElementLookupDialog/utils/Giai.ts +209 -0
- package/src/elements/ElementLookupDialog/utils/beep.ts +35 -0
- package/src/elements/ProjectSelect.tsx +128 -0
- package/src/elements/data/dsAttachments.ts +101 -0
- package/src/elements/data/dsCurrentAttachment.ts +89 -0
- package/src/elements/data/dsDamages.ts +166 -0
- package/src/elements/data/dsDamagesDuplicateCheck.ts +4 -0
- package/src/elements/data/dsElements.ts +56 -0
- package/src/elements/data/dsProjects.ts +60 -0
- package/src/elements/data/dsReportProjects.ts +4 -0
- package/src/elements/data/dsTransportElements.ts +68 -0
- package/src/elements/data/index.ts +11 -0
- package/src/elements/data/procApproveDamage.ts +19 -0
- package/src/elements/data/procDeleteAttachment.ts +19 -0
- package/src/elements/data/procDeleteDamage.ts +19 -0
- package/src/elements/data/procRemoveDamageFix.ts +19 -0
- package/src/elements/data/procRestoreAttachments.ts +19 -0
- package/src/elements/index.ts +9 -0
- package/src/index.scss +10 -10
- package/src/index.ts +13 -13
- /package/es/{Checkbox → components/Checkbox}/index.css +0 -0
- /package/es/{Checkbox → components/Checkbox}/index.d.ts +0 -0
- /package/es/{Checkbox → components/Checkbox}/index.js +0 -0
- /package/es/{ColorCard → components/ColorCard}/index.d.ts +0 -0
- /package/es/{ColorCard → components/ColorCard}/index.js +0 -0
- /package/es/{DateNavigator → components/DateNavigator}/index.css +0 -0
- /package/es/{DateNavigator → components/DateNavigator}/index.d.ts +0 -0
- /package/es/{LinkedCardList → components/LinkedCardList}/index.css +0 -0
- /package/es/{LinkedCardList → components/LinkedCardList}/index.d.ts +0 -0
- /package/es/{LinkedCardList → components/LinkedCardList}/index.js +0 -0
- /package/es/{PageBanner → components/PageBanner}/index.css +0 -0
- /package/es/{PageBanner → components/PageBanner}/index.d.ts +0 -0
- /package/es/{Portal → components/Portal}/index.d.ts +0 -0
- /package/es/{Portal → components/Portal}/index.js +0 -0
- /package/es/{ProgressBar → components/ProgressBar}/index.css +0 -0
- /package/es/{ProgressBar → components/ProgressBar}/index.d.ts +0 -0
- /package/es/{ProgressBar → components/ProgressBar}/index.js +0 -0
- /package/es/{Sidebar → components/Sidebar}/index.css +0 -0
- /package/es/{Sidebar → components/Sidebar}/index.js +0 -0
- /package/es/{Spinner → components/Spinner}/index.css +0 -0
- /package/es/{Spinner → components/Spinner}/index.d.ts +0 -0
- /package/es/{Spinner → components/Spinner}/index.js +0 -0
- /package/es/{SplitContainer → components/SplitContainer}/index.css +0 -0
- /package/es/{SplitContainer → components/SplitContainer}/index.d.ts +0 -0
- /package/es/{ValueToggleGroup → components/ValueToggleGroup}/index.d.ts +0 -0
- /package/src/{Checkbox → components/Checkbox}/index.scss +0 -0
- /package/src/{ColorCard → components/ColorCard}/index.tsx +0 -0
- /package/src/{DateNavigator → components/DateNavigator}/index.css +0 -0
- /package/src/{LinkedCardList → components/LinkedCardList}/index.tsx +0 -0
- /package/src/{Portal → components/Portal}/index.tsx +0 -0
- /package/src/{ProgressBar → components/ProgressBar}/index.tsx +0 -0
- /package/src/{Sidebar → components/Sidebar}/index.scss +0 -0
- /package/src/{Spinner → components/Spinner}/index.css +0 -0
- /package/src/{Spinner → components/Spinner}/index.tsx +0 -0
- /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
|
+
}
|