Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"react-router-dom": "^7.7.0",
"react-select": "^5.10.2",
"react-show-more-text": "^1.5.2",
"react-signature-canvas": "^1.1.0-alpha.2",
"react-simple-code-editor": "^0.14.1",
"react-tagsinput": "^3.19.0",
"redux": "^4.0.1",
Expand Down
2 changes: 2 additions & 0 deletions src/dataEntryApp/components/FormElement.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import LocationFormElement from "./LocationFormElement";
import LandingSubjectFormElement from "./LandingSubjectFormElement";
import QuestionGroupFormElement from "./QuestionGroupFormElement";
import { RepeatableQuestionGroupElement } from "./RepeatableQuestionGroupElement";
import SignatureFormElement from "./SignatureFormElement";

const StyledContainer = styled("div")(({ isGrid }) => ({
...(isGrid && {
Expand Down Expand Up @@ -42,6 +43,7 @@ const elements = {
Video: MediaFormElement,
Audio: MediaFormElement,
File: MediaFormElement,
Signature: SignatureFormElement,
Id: TextFormElement,
PhoneNumber: PhoneNumberFormElement,
Subject: LandingSubjectFormElement,
Expand Down
14 changes: 14 additions & 0 deletions src/dataEntryApp/components/Observations.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,19 @@ const Observations = ({
}}
/>
),
[Concept.dataType.Signature]: (
<img
src={signedMediaUrl}
alt={MediaData.MissingSignedMediaMessage}
align="center"
width={200}
height={200}
onClick={event => {
event.preventDefault();
showMediaOverlay(signedMediaUrl);
}}
/>
),
[Concept.dataType.Video]: (
<video
preload="auto"
Expand Down Expand Up @@ -451,6 +464,7 @@ const Observations = ({
case Concept.dataType.Audio:
return <AudioPlayer url={unsignedMediaUrl} />;
case Concept.dataType.Image:
case Concept.dataType.Signature:
case Concept.dataType.Video:
return imageVideoOptions(unsignedMediaUrl, concept);
case Concept.dataType.File:
Expand Down
181 changes: 181 additions & 0 deletions src/dataEntryApp/components/SignatureFormElement.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { useRef, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Box, Button, Paper, Typography } from "@mui/material";
import { styled } from "@mui/material/styles";
import { ValidationError } from "./ValidationError";
import { httpClient as http } from "../../common/utils/httpClient";
import { find, get } from "lodash";
// eslint-disable-next-line import/no-named-as-default
import SignatureCanvas from "react-signature-canvas";

const SignatureContainer = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
gap: theme.spacing(2)
}));

const ButtonContainer = styled(Box)(({ theme }) => ({
display: "flex",
gap: theme.spacing(1)
}));

const SignatureImage = styled("img")(({ theme }) => ({
maxWidth: "100%",
maxHeight: "200px",
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius
}));

const StyledSignatureCanvas = styled("div")(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius,
backgroundColor: "#fff",
width: "100%",
height: "200px"
}));

export default function SignatureFormElement({
formElement,
value,
update,
validationResults,
uuid
}) {
const { t } = useTranslation();
const { mandatory, name } = formElement;
const validationResult = find(
validationResults,
({ formIdentifier, questionGroupIndex }) =>
formIdentifier === uuid && questionGroupIndex === 0
);
const signatureRef = useRef(null);
const [hasSignature, setHasSignature] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [existingSignature, setExistingSignature] = useState(null);

useEffect(() => {
if (value) {
setHasSignature(true);
setExistingSignature(value);
} else {
setExistingSignature(null);
}
}, [value]);

const clearCanvas = () => {
if (signatureRef.current) {
signatureRef.current.clear();
}
setHasSignature(false);
setExistingSignature(null);
update(null);
};

const saveSignature = () => {
if (!signatureRef.current) return;

if (signatureRef.current.isEmpty()) {
alert("Please draw a signature before saving");
return;
}

setIsUploading(true);

const signatureData = signatureRef.current.getDataURL();

fetch(signatureData)
.then(res => res.blob())
.then(blob => {
const file = Object.assign(blob, {
name: "signature.png",
type: "image/png"
});

http
.uploadFile("/web/uploadMedia", file)
.then(response => {
setIsUploading(false);
setHasSignature(true);
setExistingSignature(response.data);
update(response.data);
})
.catch(error => {
setIsUploading(false);
const errorMessage =
get(error, "response.data") ||
get(error, "message") ||
"Failed to upload signature";
alert(errorMessage);
});
})
.catch(() => {
setIsUploading(false);
alert("Failed to process signature");
});
};

const handleBegin = () => {
setHasSignature(true);
};

const handleEnd = () => {};

return (
<SignatureContainer>
<Typography variant="subtitle1" color="textSecondary">
{t(name)} {mandatory && "*"}
</Typography>

{existingSignature ? (
<Paper elevation={1} sx={{ p: 2 }}>
<SignatureImage src={existingSignature} alt="Signature" />
<ButtonContainer sx={{ mt: 2 }}>
<Button
variant="outlined"
onClick={clearCanvas}
disabled={isUploading}
>
{t("Clear")}
</Button>
</ButtonContainer>
</Paper>
) : (
<Paper elevation={1} sx={{ p: 2 }}>
<StyledSignatureCanvas>
<SignatureCanvas
ref={signatureRef}
canvasProps={{
width: "100%",
height: "200px",
className: "signature-canvas"
}}
onBegin={handleBegin}
onEnd={handleEnd}
/>
</StyledSignatureCanvas>

<ButtonContainer sx={{ mt: 2 }}>
<Button
variant="outlined"
onClick={clearCanvas}
disabled={isUploading}
>
{t("Clear")}
</Button>
<Button
variant="contained"
onClick={saveSignature}
disabled={isUploading || !hasSignature}
>
{isUploading ? t("Uploading...") : t("Save Signature")}
</Button>
</ButtonContainer>
</Paper>
)}

{validationResult && (
<ValidationError validationResult={validationResult} />
)}
</SignatureContainer>
);
}
3 changes: 2 additions & 1 deletion src/formDesigner/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,6 @@ export const inlineConceptDataType = _.sortBy([
"Audio",
"File",
"QuestionGroup",
"Encounter"
"Encounter",
"Signature"
]);