Skip to content
Snippets Groups Projects
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;