15.1.764. tablet_qt/tasks/khandakermojomedical.cpp

/*
    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CamCOPS is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/

#include "khandakermojomedical.h"

#include "lib/uifunc.h"
#include "questionnairelib/commonoptions.h"
#include "questionnairelib/qudatetime.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/questionwithonefield.h"
#include "questionnairelib/qugridcontainer.h"
#include "questionnairelib/quheading.h"
#include "questionnairelib/qulineeditdouble.h"
#include "questionnairelib/qulineeditinteger.h"
#include "questionnairelib/qumcq.h"
#include "questionnairelib/qumcqgrid.h"
#include "questionnairelib/qupage.h"
#include "questionnairelib/quspacer.h"
#include "questionnairelib/qutext.h"
#include "questionnairelib/qutextedit.h"
#include "tasklib/taskfactory.h"
#include "tasklib/taskregistrar.h"


const QString KhandakerMojoMedical::KHANDAKERMOJOMEDICAL_TABLENAME(
    "khandaker_mojo_medical"
);

const QString Q_XML_PREFIX = "q_";
const QString Q_SUMMARY_XML_SUFFIX = "_s";

// Section 1: General Information
const QString FN_DIAGNOSIS("diagnosis");
const QString FN_DIAGNOSIS_DATE("diagnosis_date");
const QString FN_DIAGNOSIS_DATE_APPROXIMATE("diagnosis_date_approximate");
const QString FN_HAS_FIBROMYALGIA("has_fibromyalgia");
const QString FN_IS_PREGNANT("is_pregnant");
const QString FN_HAS_INFECTION_PAST_MONTH("has_infection_past_month");
const QString FN_HAD_INFECTION_TWO_MONTHS_PRECEDING(
    "had_infection_two_months_preceding"
);
const QString
    FN_HAS_ALCOHOL_SUBSTANCE_DEPENDENCE("has_alcohol_substance_dependence");
const QString FN_SMOKING_STATUS("smoking_status");
const QString FN_ALCOHOL_UNITS_PER_WEEK("alcohol_units_per_week");

// Section 2: Medical History
const QString FN_DEPRESSION("depression");
const QString FN_BIPOLAR_DISORDER("bipolar_disorder");
const QString FN_SCHIZOPHRENIA("schizophrenia");
const QString FN_AUTISM("autism");
const QString FN_PTSD("ptsd");
const QString FN_ANXIETY("anxiety");
const QString FN_PERSONALITY_DISORDER("personality_disorder");
const QString FN_INTELLECTUAL_DISABILITY("intellectual_disability");
const QString FN_OTHER_MENTAL_ILLNESS("other_mental_illness");
const QString FN_OTHER_MENTAL_ILLNESS_DETAILS("other_mental_illness_details");
const QString FN_HOSPITALISED_IN_LAST_YEAR("hospitalised_in_last_year");
const QString FN_HOSPITALISATION_DETAILS("hospitalisation_details");

// Section 3: Family history
const QString FN_FAMILY_DEPRESSION("family_depression");
const QString FN_FAMILY_BIPOLAR_DISORDER("family_bipolar_disorder");
const QString FN_FAMILY_SCHIZOPHRENIA("family_schizophrenia");
const QString FN_FAMILY_AUTISM("family_autism");
const QString FN_FAMILY_PTSD("family_ptsd");
const QString FN_FAMILY_ANXIETY("family_anxiety");
const QString FN_FAMILY_PERSONALITY_DISORDER("family_personality_disorder");
const QString
    FN_FAMILY_INTELLECTUAL_DISABILITY("family_intellectual_disability");
const QString FN_FAMILY_OTHER_MENTAL_ILLNESS("family_other_mental_illness");
const QString FN_FAMILY_OTHER_MENTAL_ILLNESS_DETAILS(
    "family_other_mental_illness_details"
);

const QStringList MANDATORY_FIELDNAMES{
    FN_DIAGNOSIS,
    FN_DIAGNOSIS_DATE,
    FN_HAS_FIBROMYALGIA,
    FN_IS_PREGNANT,
    FN_HAS_INFECTION_PAST_MONTH,
    FN_HAD_INFECTION_TWO_MONTHS_PRECEDING,
    FN_HAS_ALCOHOL_SUBSTANCE_DEPENDENCE,
    FN_SMOKING_STATUS,
    FN_ALCOHOL_UNITS_PER_WEEK,

    FN_DEPRESSION,
    FN_BIPOLAR_DISORDER,
    FN_SCHIZOPHRENIA,
    FN_AUTISM,
    FN_PTSD,
    FN_ANXIETY,
    FN_PERSONALITY_DISORDER,
    FN_INTELLECTUAL_DISABILITY,
    FN_OTHER_MENTAL_ILLNESS,
    FN_HOSPITALISED_IN_LAST_YEAR,

    FN_FAMILY_DEPRESSION,
    FN_FAMILY_BIPOLAR_DISORDER,
    FN_FAMILY_SCHIZOPHRENIA,
    FN_FAMILY_AUTISM,
    FN_FAMILY_PTSD,
    FN_FAMILY_ANXIETY,
    FN_FAMILY_PERSONALITY_DISORDER,
    FN_FAMILY_INTELLECTUAL_DISABILITY,
    FN_FAMILY_OTHER_MENTAL_ILLNESS,
};

const QStringList SUMMARY_FIELDNAMES{
    FN_HAS_FIBROMYALGIA,
    FN_IS_PREGNANT,
    FN_HAS_INFECTION_PAST_MONTH,
    FN_HAD_INFECTION_TWO_MONTHS_PRECEDING,
    FN_HAS_ALCOHOL_SUBSTANCE_DEPENDENCE,
};

// Maps "Other Y/N?" fields to "Other: please give details" fields
const QMap<QString, QString> DETAILS_FIELDS{
    {FN_OTHER_MENTAL_ILLNESS, FN_OTHER_MENTAL_ILLNESS_DETAILS},
    {FN_HOSPITALISED_IN_LAST_YEAR, FN_HOSPITALISATION_DETAILS},
    {FN_FAMILY_OTHER_MENTAL_ILLNESS, FN_FAMILY_OTHER_MENTAL_ILLNESS_DETAILS},
};

const int N_POSSIBLE_DIAGNOSES = 3;
const int N_SMOKING_STATUS_VALUES = 3;

void initializeKhandakerMojoMedical(TaskFactory& factory)
{
    static TaskRegistrar<KhandakerMojoMedical> registered(factory);
}

KhandakerMojoMedical::KhandakerMojoMedical(
    CamcopsApp& app, DatabaseManager& db, const int load_pk
) :
    Task(
        app, db, KHANDAKERMOJOMEDICAL_TABLENAME, false, false, false
    ),  // ... anon, clin, resp
    m_questionnaire(nullptr),
    m_fr_diagnosis_date(nullptr),
    m_fr_diagnosis_years(nullptr)
{
    // Section 1: General Information
    addField(FN_DIAGNOSIS, QMetaType::fromType<int>());
    addField(FN_DIAGNOSIS_DATE, QMetaType::fromType<QDate>());
    addField(FN_DIAGNOSIS_DATE_APPROXIMATE, QMetaType::fromType<bool>());
    addField(FN_HAS_FIBROMYALGIA, QMetaType::fromType<bool>());
    addField(FN_IS_PREGNANT, QMetaType::fromType<bool>());
    addField(FN_HAS_INFECTION_PAST_MONTH, QMetaType::fromType<bool>());
    addField(
        FN_HAD_INFECTION_TWO_MONTHS_PRECEDING, QMetaType::fromType<bool>()
    );
    addField(FN_HAS_ALCOHOL_SUBSTANCE_DEPENDENCE, QMetaType::fromType<bool>());
    addField(FN_SMOKING_STATUS, QMetaType::fromType<int>());
    addField(FN_ALCOHOL_UNITS_PER_WEEK, QMetaType::fromType<double>());

    // Section 2: Medical History
    addField(FN_DEPRESSION, QMetaType::fromType<bool>());
    addField(FN_BIPOLAR_DISORDER, QMetaType::fromType<bool>());
    addField(FN_SCHIZOPHRENIA, QMetaType::fromType<bool>());
    addField(FN_AUTISM, QMetaType::fromType<bool>());
    addField(FN_PTSD, QMetaType::fromType<bool>());
    addField(FN_ANXIETY, QMetaType::fromType<bool>());
    addField(FN_PERSONALITY_DISORDER, QMetaType::fromType<bool>());
    addField(FN_INTELLECTUAL_DISABILITY, QMetaType::fromType<bool>());
    addField(FN_OTHER_MENTAL_ILLNESS, QMetaType::fromType<bool>());
    addField(FN_OTHER_MENTAL_ILLNESS_DETAILS, QMetaType::fromType<QString>());
    addField(FN_HOSPITALISED_IN_LAST_YEAR, QMetaType::fromType<bool>());
    addField(FN_HOSPITALISATION_DETAILS, QMetaType::fromType<QString>());

    // Section 3: Family history
    addField(FN_FAMILY_DEPRESSION, QMetaType::fromType<bool>());
    addField(FN_FAMILY_BIPOLAR_DISORDER, QMetaType::fromType<bool>());
    addField(FN_FAMILY_SCHIZOPHRENIA, QMetaType::fromType<bool>());
    addField(FN_FAMILY_AUTISM, QMetaType::fromType<bool>());
    addField(FN_FAMILY_PTSD, QMetaType::fromType<bool>());
    addField(FN_FAMILY_ANXIETY, QMetaType::fromType<bool>());
    addField(FN_FAMILY_PERSONALITY_DISORDER, QMetaType::fromType<bool>());
    addField(FN_FAMILY_INTELLECTUAL_DISABILITY, QMetaType::fromType<bool>());
    addField(FN_FAMILY_OTHER_MENTAL_ILLNESS, QMetaType::fromType<bool>());
    addField(
        FN_FAMILY_OTHER_MENTAL_ILLNESS_DETAILS, QMetaType::fromType<QString>()
    );

    load(load_pk);  // MUST ALWAYS CALL from derived Task constructor.
}

// ============================================================================
// Class info
// ============================================================================

QString KhandakerMojoMedical::shortname() const
{
    return "Khandaker_MOJO_Medical";
}

QString KhandakerMojoMedical::longname() const
{
    return tr("Khandaker GM — MOJO — Medical questionnaire");
}

QString KhandakerMojoMedical::description() const
{
    return tr("Medical questionnaire for MOJO study.");
}

QString KhandakerMojoMedical::infoFilenameStem() const
{
    return "khandaker_mojo";
}

// ============================================================================
// Instance info
// ============================================================================

bool KhandakerMojoMedical::isComplete() const
{
    for (const QString& fieldname : MANDATORY_FIELDNAMES) {
        if (valueIsNull(fieldname)) {
            return false;
        }

        if (DETAILS_FIELDS.contains(fieldname)) {
            if (valueBool(fieldname)
                && valueIsNullOrEmpty(DETAILS_FIELDS.value(fieldname))) {
                return false;
            }
        }
    }

    return true;
}

QStringList KhandakerMojoMedical::summary() const
{
    QStringList lines;
    QStringList medical_history;

    const QString fmt = QString("%1 <b>%2</b>");

    for (const QString& fieldname : SUMMARY_FIELDNAMES) {
        if (valueBool(fieldname)) {
            medical_history.append(
                xstring(Q_XML_PREFIX + fieldname + Q_SUMMARY_XML_SUFFIX)
            );
        }
    }

    if (medical_history.size() > 0) {
        lines.append(
            fmt.arg(xstring("answered_yes_to"), medical_history.join(", "))
        );
    }

    lines.append(fmt.arg(xstring("q_diagnosis"), getDiagnosis()));

    return lines;
}

QString KhandakerMojoMedical::getDiagnosis() const
{
    const NameValueOptions options
        = getOptions(FN_DIAGNOSIS, N_POSSIBLE_DIAGNOSES);
    const QVariant diagnosis = value(FN_DIAGNOSIS);
    return options.nameFromValue(diagnosis, "?");
}

QStringList KhandakerMojoMedical::detail() const
{
    QStringList lines;

    for (const QString& fieldname : MANDATORY_FIELDNAMES) {
        lines.append(xstring(Q_XML_PREFIX + fieldname));
        lines.append(QString("<b>%1</b>").arg(prettyValue(fieldname)));

        if (DETAILS_FIELDS.contains(fieldname) && valueBool(fieldname)) {
            const QString details_fieldname = DETAILS_FIELDS.value(fieldname);
            lines.append(xstring(Q_XML_PREFIX + details_fieldname));
            lines.append(
                QString("<b>%1</b>").arg(prettyValue(details_fieldname))
            );
        }
    }

    return completenessInfo() + lines;
}

OpenableWidget* KhandakerMojoMedical::editor(const bool read_only)
{
    QuPagePtr page(new QuPage);

    auto heading = [this, &page](const QString& xstringname) -> void {
        page->addElement((new QuText(xstring(xstringname)))->setBold(true));
    };

    auto textQuestion = [this, &page](const QString& fieldname) -> void {
        auto text = new QuText(xstring(Q_XML_PREFIX + fieldname));
        auto text_edit = new QuTextEdit(fieldRef(fieldname));
        auto spacer
            = new QuSpacer(QSize(uiconst::BIGSPACE, uiconst::BIGSPACE));

        text->addTag(fieldname);
        text_edit->addTag(fieldname);
        spacer->addTag(fieldname);

        page->addElement(text);
        page->addElement(text_edit);
        page->addElement(spacer);
    };

    auto multiChoiceQuestion
        = [this, &page](const QString& fieldname, int num_options) -> void {
        page->addElement(new QuText(xstring(Q_XML_PREFIX + fieldname)));

        FieldRefPtr fieldref = fieldRef(fieldname);
        QuMcq* mcq = new QuMcq(fieldref, getOptions(fieldname, num_options));
        mcq->setHorizontal(true);
        page->addElement(mcq);
        page->addElement(
            new QuSpacer(QSize(uiconst::BIGSPACE, uiconst::BIGSPACE))
        );
    };

    auto yesNoQuestion = [this, &page](const QString& fieldname) -> void {
        page->addElement(new QuText(xstring(Q_XML_PREFIX + fieldname)));

        FieldRefPtr fieldref = fieldRef(fieldname);
        QuMcq* mcq = new QuMcq(fieldref, CommonOptions::noYesBoolean());
        mcq->setHorizontal(true);
        page->addElement(mcq);
        page->addElement(
            new QuSpacer(QSize(uiconst::BIGSPACE, uiconst::BIGSPACE))
        );
    };

    auto doubleQuestion = [this, &page](
                              const QString& fieldname,
                              const double minimum,
                              const double maximum,
                              const QString& hint
                          ) -> void {
        page->addElement(new QuText(xstring(Q_XML_PREFIX + fieldname)));

        auto line_edit_double
            = new QuLineEditDouble(fieldRef(fieldname), minimum, maximum);
        line_edit_double->setHint(hint);

        page->addElement(line_edit_double);
        page->addElement(
            new QuSpacer(QSize(uiconst::BIGSPACE, uiconst::BIGSPACE))
        );
    };

    auto yesNoGrid = [this, &page](const QStringList fieldnames) -> void {
        QVector<QuestionWithOneField> field_pairs;

        for (const QString& fieldname : fieldnames) {
            const QString description = xstring(Q_XML_PREFIX + fieldname);
            field_pairs.append(
                QuestionWithOneField(description, fieldRef(fieldname))
            );
        }

        auto grid = new QuMcqGrid(field_pairs, CommonOptions::noYesBoolean());

        grid->setWidth(8, {1, 1});

        grid->setSubtitles({
            {5, ""},
            {10, ""},
        });

        page->addElement(grid);
    };

    page->setTitle(description());
    page->addElement(new QuHeading(xstring("title")));
    heading("general_information_title");

    multiChoiceQuestion(FN_DIAGNOSIS, N_POSSIBLE_DIAGNOSES);

    page->addElement(getDiagnosisDateGrid());
    page->addElement(new QuSpacer(QSize(uiconst::BIGSPACE, uiconst::BIGSPACE))
    );

    heading("medical_history_title");

    yesNoQuestion(FN_HAS_FIBROMYALGIA);
    yesNoQuestion(FN_IS_PREGNANT);
    yesNoQuestion(FN_HAS_INFECTION_PAST_MONTH);
    yesNoQuestion(FN_HAD_INFECTION_TWO_MONTHS_PRECEDING);
    yesNoQuestion(FN_HAS_ALCOHOL_SUBSTANCE_DEPENDENCE);
    multiChoiceQuestion(FN_SMOKING_STATUS, N_SMOKING_STATUS_VALUES);
    doubleQuestion(
        FN_ALCOHOL_UNITS_PER_WEEK, 0, 2000, xstring("alcohol_units_hint")
    );

    yesNoQuestion(FN_HOSPITALISED_IN_LAST_YEAR);
    textQuestion(FN_HOSPITALISATION_DETAILS);

    page->addElement(new QuText(xstring("medical_history_subtitle")));
    yesNoGrid({
        FN_DEPRESSION,
        FN_BIPOLAR_DISORDER,
        FN_SCHIZOPHRENIA,
        FN_AUTISM,
        FN_PTSD,
        FN_ANXIETY,
        FN_PERSONALITY_DISORDER,
        FN_INTELLECTUAL_DISABILITY,
        FN_OTHER_MENTAL_ILLNESS,
    });

    textQuestion(FN_OTHER_MENTAL_ILLNESS_DETAILS);

    heading("family_history_title");

    page->addElement(new QuText(xstring("family_history_subtitle")));
    yesNoGrid({
        FN_FAMILY_DEPRESSION,
        FN_FAMILY_BIPOLAR_DISORDER,
        FN_FAMILY_SCHIZOPHRENIA,
        FN_FAMILY_AUTISM,
        FN_FAMILY_PTSD,
        FN_FAMILY_ANXIETY,
        FN_FAMILY_PERSONALITY_DISORDER,
        FN_FAMILY_INTELLECTUAL_DISABILITY,
        FN_FAMILY_OTHER_MENTAL_ILLNESS,
    });

    textQuestion(FN_FAMILY_OTHER_MENTAL_ILLNESS_DETAILS);

    const auto fieldnames = DETAILS_FIELDS.keys();
    for (const auto& fieldname : fieldnames) {
        FieldRefPtr fieldref = fieldRef(fieldname);

        connect(
            fieldref.data(),
            &FieldRef::valueChanged,
            this,
            &KhandakerMojoMedical::updateMandatory
        );
    }

    QVector<QuPagePtr> pages{page};

    m_questionnaire = new Questionnaire(m_app, pages);
    m_questionnaire->setType(QuPage::PageType::Patient);
    m_questionnaire->setReadOnly(read_only);

    updateMandatory();
    updateDurationOfIllness();

    return m_questionnaire;
}

QuGridContainer* KhandakerMojoMedical::getDiagnosisDateGrid()
{
    FieldRef::GetterFunction get_date
        = std::bind(&KhandakerMojoMedical::getDiagnosisDate, this);
    FieldRef::GetterFunction get_years
        = std::bind(&KhandakerMojoMedical::getDurationOfIllness, this);
    FieldRef::SetterFunction set_date = std::bind(
        &KhandakerMojoMedical::setDiagnosisDate, this, std::placeholders::_1
    );
    FieldRef::SetterFunction set_years = std::bind(
        &KhandakerMojoMedical::setDurationOfIllness,
        this,
        std::placeholders::_1
    );

    m_fr_diagnosis_date = FieldRefPtr(new FieldRef(get_date, set_date, true));
    m_fr_diagnosis_years
        = FieldRefPtr(new FieldRef(get_years, set_years, true));

    auto diagnosis_date_grid = new QuGridContainer();
    diagnosis_date_grid->setFixedGrid(true);

    // We don't store duration of illness on the server
    auto duration_text
        = new QuText(xstring("duration_of_illness_or_diagnosis_date"));
    auto diagnosis_years = new QuLineEditInteger(m_fr_diagnosis_years, 0, 150);

    auto date_time = new QuDateTime(m_fr_diagnosis_date);
    date_time->setOfferNowButton(true);
    date_time->setMode(QuDateTime::Mode::DefaultDate);
    date_time->setMaximumDate(QDate::currentDate());

    diagnosis_date_grid->addCell(QuGridCell(duration_text, 0, 0, 1, 2));

    diagnosis_date_grid->addCell(QuGridCell(diagnosis_years, 1, 0));
    diagnosis_date_grid->addCell(QuGridCell(date_time, 1, 1));
    diagnosis_date_grid->setColumnStretch(0, 1);
    diagnosis_date_grid->setColumnStretch(1, 4);

    return diagnosis_date_grid;
}

QVariant KhandakerMojoMedical::getDiagnosisDate() const
{
    return value(FN_DIAGNOSIS_DATE);
}

QVariant KhandakerMojoMedical::getDurationOfIllness() const
{
    return m_diagnosis_years;
}

bool KhandakerMojoMedical::setDiagnosisDate(const QVariant& value)
{
    const bool changed = setValue(FN_DIAGNOSIS_DATE, value);
    if (changed) {
        setValue(FN_DIAGNOSIS_DATE_APPROXIMATE, false);
        updateDurationOfIllness();
    }

    return changed;
}

bool KhandakerMojoMedical::setDurationOfIllness(const QVariant& value)
{
    Q_ASSERT(m_fr_diagnosis_years);
    const bool changed = value != m_diagnosis_years;
    if (changed) {
        m_diagnosis_years = value;
        setValue(FN_DIAGNOSIS_DATE_APPROXIMATE, true);
        updateDiagnosisDate();
    }

    return changed;
}

void KhandakerMojoMedical::updateDiagnosisDate()
{
    Q_ASSERT(m_fr_diagnosis_date);
    if (m_diagnosis_years.isNull()) {
        setValue(FN_DIAGNOSIS_DATE, QVariant());
    } else {
        const int years = m_diagnosis_years.toInt();
        setValue(FN_DIAGNOSIS_DATE, QDate::currentDate().addYears(-years));
    }
    m_fr_diagnosis_date->emitValueChanged();
}

void KhandakerMojoMedical::updateDurationOfIllness()
{
    Q_ASSERT(m_fr_diagnosis_years);
    const QVariant diagnosis_date = value(FN_DIAGNOSIS_DATE);
    if (diagnosis_date.isNull()) {
        m_diagnosis_years.clear();
    } else {
        const double days
            = diagnosis_date.toDate().daysTo(QDate::currentDate());
        m_diagnosis_years = static_cast<int>(floor(0.5 + days / 365.25));
    }
    m_fr_diagnosis_years->emitValueChanged();
}

QString KhandakerMojoMedical::getOptionName(
    const QString& fieldname, const int index
) const
{
    return xstring(QString("%1_%2").arg(fieldname).arg(index));
}

NameValueOptions KhandakerMojoMedical::getOptions(
    const QString& fieldname, const int num_options
) const
{
    NameValueOptions options;
    for (int i = 0; i < num_options; i++) {
        const QString name = getOptionName(fieldname, i);
        options.append(NameValuePair(name, i));
    }
    return options;
}

// ============================================================================
// Signal handlers
// ============================================================================

void KhandakerMojoMedical::updateMandatory()
{
    // This could be more efficient with lots of signal handlers, but...

    const auto fieldnames = DETAILS_FIELDS.keys();
    for (const auto& fieldname : fieldnames) {
        /*
        // Removed this, thus only showing details when "other Y" chosen
        if (valueIsNull(fieldname)) {
            continue;
        }
        */

        const bool mandatory = valueBool(fieldname);
        const QString details_fieldname = DETAILS_FIELDS.value(fieldname);
        fieldRef(details_fieldname)->setMandatory(mandatory);

        m_questionnaire->setVisibleByTag(details_fieldname, mandatory);
    }
}