Skip to content

Commit c5b2f66

Browse files
committed
add support for signature concept
1 parent a68621c commit c5b2f66

File tree

5 files changed

+200
-1
lines changed

5 files changed

+200
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"react-router-dom": "^7.7.0",
6464
"react-select": "^5.10.2",
6565
"react-show-more-text": "^1.5.2",
66+
"react-signature-canvas": "^1.1.0-alpha.2",
6667
"react-simple-code-editor": "^0.14.1",
6768
"react-tagsinput": "^3.19.0",
6869
"redux": "^4.0.1",

src/dataEntryApp/components/FormElement.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import LocationFormElement from "./LocationFormElement";
1515
import LandingSubjectFormElement from "./LandingSubjectFormElement";
1616
import QuestionGroupFormElement from "./QuestionGroupFormElement";
1717
import { RepeatableQuestionGroupElement } from "./RepeatableQuestionGroupElement";
18+
import SignatureFormElement from "./SignatureFormElement";
1819

1920
const StyledContainer = styled("div")(({ isGrid }) => ({
2021
...(isGrid && {
@@ -42,6 +43,7 @@ const elements = {
4243
Video: MediaFormElement,
4344
Audio: MediaFormElement,
4445
File: MediaFormElement,
46+
Signature: SignatureFormElement,
4547
Id: TextFormElement,
4648
PhoneNumber: PhoneNumberFormElement,
4749
Subject: LandingSubjectFormElement,

src/dataEntryApp/components/Observations.jsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,19 @@ const Observations = ({
343343
}}
344344
/>
345345
),
346+
[Concept.dataType.Signature]: (
347+
<img
348+
src={signedMediaUrl}
349+
alt={MediaData.MissingSignedMediaMessage}
350+
align="center"
351+
width={200}
352+
height={200}
353+
onClick={event => {
354+
event.preventDefault();
355+
showMediaOverlay(signedMediaUrl);
356+
}}
357+
/>
358+
),
346359
[Concept.dataType.Video]: (
347360
<video
348361
preload="auto"
@@ -451,6 +464,7 @@ const Observations = ({
451464
case Concept.dataType.Audio:
452465
return <AudioPlayer url={unsignedMediaUrl} />;
453466
case Concept.dataType.Image:
467+
case Concept.dataType.Signature:
454468
case Concept.dataType.Video:
455469
return imageVideoOptions(unsignedMediaUrl, concept);
456470
case Concept.dataType.File:
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { useRef, useEffect, useState } from "react";
2+
import { useTranslation } from "react-i18next";
3+
import { Box, Button, Paper, Typography } from "@mui/material";
4+
import { styled } from "@mui/material/styles";
5+
import { ValidationError } from "./ValidationError";
6+
import { httpClient as http } from "../../common/utils/httpClient";
7+
import { find, get } from "lodash";
8+
// eslint-disable-next-line import/no-named-as-default
9+
import SignatureCanvas from "react-signature-canvas";
10+
11+
const SignatureContainer = styled(Box)(({ theme }) => ({
12+
display: "flex",
13+
flexDirection: "column",
14+
gap: theme.spacing(2)
15+
}));
16+
17+
const ButtonContainer = styled(Box)(({ theme }) => ({
18+
display: "flex",
19+
gap: theme.spacing(1)
20+
}));
21+
22+
const SignatureImage = styled("img")(({ theme }) => ({
23+
maxWidth: "100%",
24+
maxHeight: "200px",
25+
border: `1px solid ${theme.palette.divider}`,
26+
borderRadius: theme.shape.borderRadius
27+
}));
28+
29+
const StyledSignatureCanvas = styled("div")(({ theme }) => ({
30+
border: `1px solid ${theme.palette.divider}`,
31+
borderRadius: theme.shape.borderRadius,
32+
backgroundColor: "#fff",
33+
width: "100%",
34+
height: "200px"
35+
}));
36+
37+
export default function SignatureFormElement({
38+
formElement,
39+
value,
40+
update,
41+
validationResults,
42+
uuid
43+
}) {
44+
const { t } = useTranslation();
45+
const { mandatory, name } = formElement;
46+
const validationResult = find(
47+
validationResults,
48+
({ formIdentifier, questionGroupIndex }) =>
49+
formIdentifier === uuid && questionGroupIndex === 0
50+
);
51+
const signatureRef = useRef(null);
52+
const [hasSignature, setHasSignature] = useState(false);
53+
const [isUploading, setIsUploading] = useState(false);
54+
const [existingSignature, setExistingSignature] = useState(null);
55+
56+
useEffect(() => {
57+
if (value) {
58+
setHasSignature(true);
59+
setExistingSignature(value);
60+
} else {
61+
setExistingSignature(null);
62+
}
63+
}, [value]);
64+
65+
const clearCanvas = () => {
66+
if (signatureRef.current) {
67+
signatureRef.current.clear();
68+
}
69+
setHasSignature(false);
70+
setExistingSignature(null);
71+
update(null);
72+
};
73+
74+
const saveSignature = () => {
75+
if (!signatureRef.current) return;
76+
77+
if (signatureRef.current.isEmpty()) {
78+
alert("Please draw a signature before saving");
79+
return;
80+
}
81+
82+
setIsUploading(true);
83+
84+
const signatureData = signatureRef.current.getDataURL();
85+
86+
fetch(signatureData)
87+
.then(res => res.blob())
88+
.then(blob => {
89+
const file = Object.assign(blob, {
90+
name: "signature.png",
91+
type: "image/png"
92+
});
93+
94+
http
95+
.uploadFile("/web/uploadMedia", file)
96+
.then(response => {
97+
setIsUploading(false);
98+
setHasSignature(true);
99+
setExistingSignature(response.data);
100+
update(response.data);
101+
})
102+
.catch(error => {
103+
setIsUploading(false);
104+
const errorMessage =
105+
get(error, "response.data") ||
106+
get(error, "message") ||
107+
"Failed to upload signature";
108+
alert(errorMessage);
109+
});
110+
})
111+
.catch(() => {
112+
setIsUploading(false);
113+
alert("Failed to process signature");
114+
});
115+
};
116+
117+
const handleBegin = () => {
118+
setHasSignature(true);
119+
};
120+
121+
const handleEnd = () => {};
122+
123+
return (
124+
<SignatureContainer>
125+
<Typography variant="subtitle1" color="textSecondary">
126+
{t(name)} {mandatory && "*"}
127+
</Typography>
128+
129+
{existingSignature ? (
130+
<Paper elevation={1} sx={{ p: 2 }}>
131+
<SignatureImage src={existingSignature} alt="Signature" />
132+
<ButtonContainer sx={{ mt: 2 }}>
133+
<Button
134+
variant="outlined"
135+
onClick={clearCanvas}
136+
disabled={isUploading}
137+
>
138+
{t("Clear")}
139+
</Button>
140+
</ButtonContainer>
141+
</Paper>
142+
) : (
143+
<Paper elevation={1} sx={{ p: 2 }}>
144+
<StyledSignatureCanvas>
145+
<SignatureCanvas
146+
ref={signatureRef}
147+
canvasProps={{
148+
width: "100%",
149+
height: "200px",
150+
className: "signature-canvas"
151+
}}
152+
onBegin={handleBegin}
153+
onEnd={handleEnd}
154+
/>
155+
</StyledSignatureCanvas>
156+
157+
<ButtonContainer sx={{ mt: 2 }}>
158+
<Button
159+
variant="outlined"
160+
onClick={clearCanvas}
161+
disabled={isUploading}
162+
>
163+
{t("Clear")}
164+
</Button>
165+
<Button
166+
variant="contained"
167+
onClick={saveSignature}
168+
disabled={isUploading || !hasSignature}
169+
>
170+
{isUploading ? t("Uploading...") : t("Save Signature")}
171+
</Button>
172+
</ButtonContainer>
173+
</Paper>
174+
)}
175+
176+
{validationResult && (
177+
<ValidationError validationResult={validationResult} />
178+
)}
179+
</SignatureContainer>
180+
);
181+
}

src/formDesigner/common/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,6 @@ export const inlineConceptDataType = _.sortBy([
8585
"Audio",
8686
"File",
8787
"QuestionGroup",
88-
"Encounter"
88+
"Encounter",
89+
"Signature"
8990
]);

0 commit comments

Comments
 (0)