Skip to content
Snippets Groups Projects
Select Git revision
  • acf7a622219b151eb7e3d961e509f5613495d203
  • develop default
  • master protected
  • feature/frontend-tests
  • 0.106
  • 0.105
  • 0.104
  • 0.103
  • 0.102
  • 0.101
  • 0.100
  • 0.99
  • 0.98
  • 0.97
  • 0.96
  • 0.95
  • 0.94
  • 0.93
  • 0.92
  • 0.91
  • 0.90
  • 0.89
  • 0.88
  • 0.87
24 results

SurveyContainerComponent.tsx

Blame
  • SurveyContainerComponent.tsx 11.33 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";
    import { FunctionFactory } from "survey-core";
    import { validationFunctions } from "./validation/validation";
    
    
    interface ValidationQuestion {
        name?: string;
        value?: any;
        data?: ValidationQuestion;
    }
    
    // Overrides for questions that need to be validated differently from the default expression in their group
    const questionOverrides = {
        data_protection_contact: (...args) => true, // don't validate the contact field, anything goes..
    }
    
    function validateQuestion(this: { question: ValidationQuestion, row?: any }, params: any) {
        try {
            const question = this.question;
            const validator = params[0] || undefined;
    
            const matrix = question.data && 'name' in question.data;
    
            let questionName;
    
            if (matrix) {
                questionName = question.data!.name;
            } else {
                questionName = question.name;
            }
            const value = question.value
    
            const hasOverride = questionOverrides[questionName];
            if (hasOverride) {
                return hasOverride(value, ...params.slice(1));
            }
    
            const validationFunction = validationFunctions[validator];
            if (!validationFunction) {
                throw new Error(`Validation function ${validator} not found for question ${questionName}`);
            }
    
            return validationFunction(value, ...params.slice(1));
        } catch (e) {
            console.error(e);
            return false;
        }
    }
    
    
    
    
    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...');
    
        if (!FunctionFactory.Instance.hasFunction("validateQuestion")) {
            FunctionFactory.Instance.register("validateQuestion", validateQuestion);
        }
    
        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, validateStatus = true) => {
            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!';
                }
            };
            if (validateStatus) surveyModel.onValidateQuestion.add(verificationValidator);
            const validSurvey = validatorFunction();
            if (validateStatus) surveyModel.onValidateQuestion.remove(verificationValidator);
            if (!validSurvey) {
                toast("Validation failed!");
            }
            return validSurvey;
        }
    
        const surveyActions = {
            'save': async () => {
                const allFieldsValid = validateWithAnswerVerification(surveyModel.validate.bind(surveyModel, true, true), false);
                if (!allFieldsValid) {
                    toast("Please correct the invalid fields before saving!");
                    return;
                }
                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 allFieldsValid = validateWithAnswerVerification(surveyModel.validate.bind(surveyModel, true, true), false);
                if (!allFieldsValid) {
                    toast("Please correct the invalid fields before saving.");
                    return;
                }
                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'];
                // Validate when we start editing to ensure invalid fields are corrected by the user
                const allFieldsValid = validateWithAnswerVerification(surveyModel.validate.bind(surveyModel, true, true), false);
                if (!allFieldsValid) {
                    toast("Some fields are invalid, please correct them.");
                    return;
                }
    
            },
            '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;