-
Bjarke Madsen authored
Uses code snippets partly from matomo-tracker-react since it's no longer maintained
Bjarke Madsen authoredUses code snippets partly from matomo-tracker-react since it's no longer maintained
SurveyContainerComponent.tsx 8.82 KiB
import React, { useEffect, useState, useCallback } from "react";
import { Container } from "react-bootstrap";
import toast, { Toaster } from "react-hot-toast";
import { Model, Serializer } from "survey-core";
import { useParams } from "react-router-dom";
import SurveyComponent from "./SurveyComponent";
import SurveyNavigationComponent from "./SurveyNavigationComponent";
import { VerificationStatus } from './Schema';
import Prompt from "./Prompt";
import "survey-core/modern.min.css";
import './survey.scss';
import useMatomo from "../matomo/UseMatomo";
Serializer.addProperty("itemvalue", "customDescription:text");
Serializer.addProperty("question", "hideCheckboxLabels:boolean");
function SurveyContainerComponent({ loadFrom }) {
const [surveyModel, setSurveyModel] = useState<Model>(); // note that this is never updated and we abuse that fact by adding extra state to the surveyModel
const { year, nren } = useParams(); // nren stays empty for inspect and try
const [error, setError] = useState<string>('loading survey...');
const { trackPageView } = useMatomo();
const beforeUnloadListener = useCallback((event) => {
event.preventDefault();
return (event.returnValue = "");
}, []);
const pageHideListener = useCallback(() => {
window.navigator.sendBeacon('/api/response/unlock/' + year + '/' + nren);
}, []);
const onPageExitThroughRouter = useCallback(() => {
window.navigator.sendBeacon('/api/response/unlock/' + year + '/' + nren);
removeEventListener("beforeunload", beforeUnloadListener, { capture: true });
removeEventListener("pagehide", pageHideListener);
}, []);
useEffect(() => {
async function getModel() {
const response = await fetch(loadFrom + year + (nren ? '/' + nren : ''))
const json = await response.json();
if (!response.ok) {
if ('message' in json) {
throw new Error(json.message);
} else {
throw new Error(`Request failed with status ${response.status}`);
}
}
const survey = new Model(json['model']);
survey.setVariable('surveyyear', year);
survey.setVariable('previousyear', parseInt(year!) - 1);
survey.showNavigationButtons = false;
survey.requiredText = '';
survey.verificationStatus = new Map<string, VerificationStatus>();
for (const questionName in json["verification_status"]) {
survey.verificationStatus.set(questionName, json["verification_status"][questionName]);
}
survey.data = json['data'];
survey.clearIncorrectValues(true);
survey.currentPageNo = json['page'];
survey.mode = json['mode'];
survey.lockedBy = json['locked_by'];
survey.status = json['status'];
survey.editAllowed = json['edit_allowed'];
setSurveyModel(survey);
}
getModel().catch(error => setError('Error when loading survey: ' + error.message)).then(() => {
trackPageView({ documentTitle: `Survey for ${nren} (${year})` });
})
}, []);
if (!surveyModel) {
return <>{error}</>
}
const saveSurveyData = async (survey, newState) => {
if (!nren) {
return "Saving not available in inpect/try mode";
}
const saveData = {
lock_uuid: survey.lockUUID,
new_state: newState,
data: survey.data,
page: survey.currentPageNo,
verification_status: Object.fromEntries(survey.verificationStatus)
};
try {
const response = await fetch(
'/api/response/save/' + year + '/' + nren,
{ method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, body: JSON.stringify(saveData) }
);
const json = await response.json();
if (!response.ok) {
return json['message'];
}
surveyModel.mode = json['mode'];
surveyModel.lockedBy = json['locked_by'];
surveyModel.status = json['status'];
} catch (e) {
return "Unknown Error: " + (e as Error).message;
}
}
const validateWithAnswerVerification = (validatorFunction) => {
let firstValidationError = '';
const verificationValidator = (survey, options) => {
const status = survey.verificationStatus.get(options.name);
if (status == VerificationStatus.Unverified) {
if (firstValidationError == '') {
firstValidationError = options.name;
}
options.error = 'Please verify that last years data is correct by editing the answer or pressing the "No change from previous year" button!';
}
};
surveyModel.onValidateQuestion.add(verificationValidator);
const validSurvey = validatorFunction();
surveyModel.onValidateQuestion.remove(verificationValidator);
if (!validSurvey) {
toast("Validation failed!");
}
return validSurvey;
}
const surveyActions = {
'save': async () => {
const errorMessage = await saveSurveyData(surveyModel, "editing");
if (errorMessage) {
toast("Failed saving survey: " + errorMessage);
} else {
toast("Survey saved!");
}
},
'complete': async () => {
const validSurvey = validateWithAnswerVerification(surveyModel.validate.bind(surveyModel, true, true));
if (validSurvey) {
const errorMessage = await saveSurveyData(surveyModel, "completed");
if (errorMessage) {
toast("Failed completing survey: " + errorMessage);
} else {
toast("Survey completed!");
removeEventListener("beforeunload", beforeUnloadListener, { capture: true });
removeEventListener("pagehide", pageHideListener);
}
}
},
'saveAndStopEdit': async () => {
const errorMessage = await saveSurveyData(surveyModel, "readonly");
if (errorMessage) {
toast("Failed saving survey: " + errorMessage);
} else {
toast("Survey saved!");
removeEventListener("beforeunload", beforeUnloadListener, { capture: true });
removeEventListener("pagehide", pageHideListener);
}
},
'startEdit': async () => {
const response = await fetch('/api/response/lock/' + year + '/' + nren, { method: "POST" });
const json = await response.json();
if (!response.ok) {
toast("Failed starting edit: " + json['message']);
return;
}
addEventListener("pagehide", pageHideListener);
addEventListener("beforeunload", beforeUnloadListener, { capture: true });
for (const questionName in json["verification_status"]) {
surveyModel.verificationStatus.set(questionName, json["verification_status"][questionName]);
}
surveyModel.data = json['data'];
surveyModel.clearIncorrectValues(true);
surveyModel.mode = json['mode'];
surveyModel.lockedBy = json['locked_by']
surveyModel.lockUUID = json['lock_uuid'];
surveyModel.status = json['status'];
},
'releaseLock': async () => {
const response = await fetch('/api/response/unlock/' + year + '/' + nren, { method: 'POST' });
const json = await response.json();
if (!response.ok) {
toast("Failed releasing lock: " + json['message']);
return;
}
surveyModel.mode = json['mode'];
surveyModel.lockedBy = json['locked_by'];
surveyModel.status = json['status'];
},
'validatePage': () => {
const validSurvey = validateWithAnswerVerification(surveyModel.validatePage.bind(surveyModel));
if (validSurvey) {
toast("Page validation successful!");
}
}
}
return (
<Container className="survey-container">
<Toaster />
<Prompt message="Are you sure you want to leave this page? Information you've entered may not be saved." when={() => { return surveyModel.mode == 'edit' && !!nren; }} onPageExit={onPageExitThroughRouter} />
<SurveyNavigationComponent surveyModel={surveyModel} surveyActions={surveyActions} year={year} nren={nren}>
<SurveyComponent surveyModel={surveyModel} />
</SurveyNavigationComponent>
</Container>
);
}
export default SurveyContainerComponent;