Select Git revision
      
  SurveyContainerComponent.tsx
  SurveyContainerComponent.tsx  8.35 KiB 
import React, { useEffect, useRef, 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 "survey-core/modern.min.css";
import './survey.scss';
Serializer.addProperty("itemvalue", "customDescription:text");
Serializer.addProperty("question", "hideCheckboxLabels:boolean");
function SurveyContainerComponent({ loadFrom }) {
    // NB This component should only rerender after the model is retrieved.
    //    If you change this to rerender more often, stuff will probably break.
    const [surveyModel, setSurveyModel] = useState<Model>();
    const verificationStatus = useRef<Map<string, VerificationStatus>>(new Map());
    const { year, nren } = useParams();  // year is always set, nren stays empty for inspect and try
    const [error, setError] = useState<string>('loading survey...');
    const lockUUID = useRef<string>();
    const beforeUnloadListener = useCallback((event) => {
        event.preventDefault();
        return (event.returnValue = "");
    }, []);
    const pageHideListener = useCallback(() => {
        if (!nren) {
            return;
        }
        window.navigator.sendBeacon('/api/response/unlock/' + year + '/' + nren);
    }, []);
    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}`);
                }
            }
            for (const questionName in json["verification_status"]) {
                verificationStatus.current.set(questionName, json["verification_status"][questionName]);
            }
            const survey = new Model(json['model']);
            survey.setVariable('surveyyear', year);
            survey.setVariable('previousyear', parseInt(year!) - 1);
            survey.data = json['data'];
            survey.clearIncorrectValues(true);  // TODO test if this really removes all old values and such
            survey.currentPageNo = json['page'];
            survey.showNavigationButtons = false;
            survey.showTOC = false;
            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))
    }, []);
    if (!surveyModel) {
        return error
    }
    const saveSurveyData = async (survey, newState) => {
        if (!nren) {
            return "Saving not available in inpect/try mode";
        }
        const saveData = {
            lock_uuid: lockUUID.current,
            new_state: newState,
            data: survey.data,
            page: survey.currentPageNo,
            verification_status: Object.fromEntries(verificationStatus.current)
        };
        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 = verificationStatus.current.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) {
            // perhaps first focus normal validation errors and only afterwards the verification validation errors?
            toast("Validation failed!");
            surveyModel.focusQuestion(firstValidationError);
        }
        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 });
            surveyModel.data = json['data'];
            surveyModel.clearIncorrectValues(true);
            surveyModel.mode = json['mode'];
            surveyModel.lockedBy = json['locked_by']
            surveyModel.status = json['status'];
            lockUUID.current = json['lock_uuid'];
        },
        '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 />
            <SurveyNavigationComponent surveyModel={surveyModel} surveyActions={surveyActions} year={year} nren={nren}>
                <SurveyComponent surveyModel={surveyModel} verificationStatus={verificationStatus} />
            </SurveyNavigationComponent>
        </Container>
    );
}
export default SurveyContainerComponent;