15.1.714. tablet_qt/tasks/gbogras.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 "gbogras.h"

#include "lib/datetime.h"
#include "lib/stringfunc.h"
#include "maths/mathfunc.h"
#include "questionnairelib/quboolean.h"
#include "questionnairelib/qudatetime.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/quflowcontainer.h"
#include "questionnairelib/quheading.h"
#include "questionnairelib/quhorizontalline.h"
#include "questionnairelib/qumcq.h"
#include "questionnairelib/quslider.h"
#include "questionnairelib/quspacer.h"
#include "questionnairelib/qutext.h"
#include "questionnairelib/qutextedit.h"
#include "tasklib/taskfactory.h"
#include "tasklib/taskregistrar.h"
#include "taskxtra/gbocommon.h"

using mathfunc::anyNullOrEmpty;
using stringfunc::strseq;

const QString GboGRaS::GBOGRAS_TABLENAME("gbogras");

const QString FN_DATE("date");
const QString FN_RATE_GOAL_1("rate_goal_1");
const QString FN_RATE_GOAL_2("rate_goal_2");
const QString FN_RATE_GOAL_3("rate_goal_3");
const QString FN_GOAL_1_DESC("goal_1_description");
const QString FN_GOAL_2_DESC("goal_2_description");
const QString FN_GOAL_3_DESC("goal_3_description");
const QString FN_GOAL_1_PROGRESS("goal_1_progress");
const QString FN_GOAL_2_PROGRESS("goal_2_progress");
const QString FN_GOAL_3_PROGRESS("goal_3_progress");
const QString FN_COMPLETED_BY("completed_by");
const QString FN_COMPLETED_BY_OTHER("completed_by_other");

const QString TAG_OTHER("other");

// ============================================================================
// Helper functions
// ============================================================================

QString getGoalTag(const int goalnum)
{
    return QString("goal%1").arg(goalnum);
}

// ============================================================================
// Registration
// ============================================================================

void initializeGboGRaS(TaskFactory& factory)
{
    static TaskRegistrar<GboGRaS> registered(factory);
}

// ============================================================================
// Constructor
// ============================================================================

GboGRaS::GboGRaS(CamcopsApp& app, DatabaseManager& db, const int load_pk) :
    Task(app, db, GBOGRAS_TABLENAME, false, false, false),
    // ... anon, clin, resp
    m_questionnaire(nullptr)
{
    addField(FN_DATE, QMetaType::fromType<QDate>());
    addField(FN_RATE_GOAL_1, QMetaType::fromType<bool>());
    addField(FN_RATE_GOAL_2, QMetaType::fromType<bool>());
    addField(FN_RATE_GOAL_3, QMetaType::fromType<bool>());
    addField(FN_GOAL_1_DESC, QMetaType::fromType<QString>());
    addField(FN_GOAL_2_DESC, QMetaType::fromType<QString>());
    addField(FN_GOAL_3_DESC, QMetaType::fromType<QString>());
    addField(FN_GOAL_1_PROGRESS, QMetaType::fromType<int>());
    addField(FN_GOAL_2_PROGRESS, QMetaType::fromType<int>());
    addField(FN_GOAL_3_PROGRESS, QMetaType::fromType<int>());
    addField(FN_COMPLETED_BY, QMetaType::fromType<int>());
    addField(FN_COMPLETED_BY_OTHER, QMetaType::fromType<QString>());
    load(load_pk);  // MUST ALWAYS CALL from derived Task constructor.

    if (load_pk == dbconst::NONEXISTENT_PK) {
        setValue(FN_DATE, datetime::nowDate(), false);
    }
}

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

QString GboGRaS::shortname() const
{
    return "GBO-GRaS";
}

QString GboGRaS::longname() const
{
    return tr("Goal-Based Outcomes – 3 – Goal Rating Sheet");
}

QString GboGRaS::description() const
{
    return tr(
        "For recording progress towards the goals of therapy "
        "(up to 3 goals)."
    );
}

QString GboGRaS::infoFilenameStem() const
{
    return xstringTaskname();
}

QString GboGRaS::xstringTaskname() const
{
    return "gbo";
}

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

bool GboGRaS::isComplete() const
{
    if (anyValuesNullOrEmpty({FN_DATE, FN_COMPLETED_BY})) {
        return false;
    }
    if (value(FN_COMPLETED_BY) == gbocommon::AGENT_OTHER
        && valueIsNullOrEmpty(FN_COMPLETED_BY_OTHER)) {
        return false;
    }
    int n_goals_completed = 0;

    auto goalOK = [this, &n_goals_completed](
                      const QString& fn_rate,
                      const QString& fn_desc,
                      const QString& fn_progress
                  ) -> bool {
        if (valueBool(fn_rate)) {
            ++n_goals_completed;
            if (anyValuesNullOrEmpty({fn_desc, fn_progress})) {
                return false;
            }
        }
        return true;
    };

    if (!goalOK(FN_RATE_GOAL_1, FN_GOAL_1_DESC, FN_GOAL_1_PROGRESS)) {
        return false;
    }
    if (!goalOK(FN_RATE_GOAL_2, FN_GOAL_2_DESC, FN_GOAL_2_PROGRESS)) {
        return false;
    }
    if (!goalOK(FN_RATE_GOAL_3, FN_GOAL_3_DESC, FN_GOAL_3_PROGRESS)) {
        return false;
    }
    return n_goals_completed > 0;
}

QStringList GboGRaS::summary() const
{
    QStringList lines;

    auto doGoal = [this, &lines](
                      const int goalnum,
                      const QString& fn_rate,
                      const QString& fn_progress
                  ) -> void {
        if (!valueBool(fn_rate)) {
            return;
        }
        QString line = QString("Goal <b>%1</b>: progress <b>%2</b>/%3.")
                           .arg(
                               QString::number(goalnum),
                               prettyValue(fn_progress),
                               QString::number(gbocommon::PROGRESS_MAX)
                           );
        lines.append(line);
    };

    lines.append(QString("Date: <b>%1</b>.")
                     .arg(datetime::dateToIso(valueDate(FN_DATE))));
    doGoal(1, FN_RATE_GOAL_1, FN_GOAL_1_PROGRESS);
    doGoal(2, FN_RATE_GOAL_2, FN_GOAL_2_PROGRESS);
    doGoal(3, FN_RATE_GOAL_3, FN_GOAL_3_PROGRESS);
    return lines;
}

QStringList GboGRaS::detail() const
{
    return summary();
}

OpenableWidget* GboGRaS::editor(const bool read_only)
{
    // ------------------------------------------------------------------------
    // Define options
    // ------------------------------------------------------------------------

    const NameValueOptions completed_by_options = NameValueOptions{
        {xstring("agent_1"), gbocommon::AGENT_PATIENT},
        {xstring("agent_2"), gbocommon::AGENT_PARENT_CARER},
        // not 3: clinician
        {xstring("agent_4"), gbocommon::AGENT_OTHER},
    };

    QMap<int, QString> slider_tick_map;
    for (int r = gbocommon::PROGRESS_MIN; r <= gbocommon::PROGRESS_MAX; ++r) {
        QString label;
        switch (r) {
            case 0:
            case 5:
            case 10:
                label = xstring(QString("gras_anchor_%1").arg(r));
                break;
            default:
                break;
        }
        label = label + "\n" + QString::number(r);
        slider_tick_map[r] = label;
    }

    // ------------------------------------------------------------------------
    // Starting elements
    // ------------------------------------------------------------------------

    QVector<QuElement*> elements{
        new QuFlowContainer{
            new QuHeading(xstring("date")),
            (new QuDateTime(fieldRef(FN_DATE)))
                ->setMode(QuDateTime::DefaultDate)
                ->setOfferNowButton(true),
        },
        (new QuText(xstring("gras_question")))
            ->setBig()
            ->setBold()
            ->setItalic(),
        new QuText(xstring("gras_instruction")),
        (new QuText(xstring("progress_explanation")))->setItalic(),
        new QuSpacer(),
    };

    // ------------------------------------------------------------------------
    // Goal rating elements
    // ------------------------------------------------------------------------

    auto addGoal = [this, &elements, &slider_tick_map](
                       const int goalnum,
                       const QString& fn_rate,
                       const QString& fn_desc,
                       const QString& fn_progress
                   ) -> void {
        const QString tag = getGoalTag(goalnum);
        // I tried with a QuMcqGrid but a slider is much better at an evenly
        // distributed set of responses where some have (textually lengthy)
        // anchor points.
        QuSlider* slider = new QuSlider(
            fieldRef(fn_progress),
            gbocommon::PROGRESS_MIN,
            gbocommon::PROGRESS_MAX
        );
        slider->setTickPosition(QSlider::TicksBothSides);
        slider->setTickLabelPosition(QSlider::TicksAbove);
        slider->addTag(tag);
        slider->setTickLabels(slider_tick_map);
        slider->setBigStep(1);
        elements.append({
            new QuHeading(
                xstring(QString("goal_rating_heading_%1").arg(goalnum))
            ),
            (new QuBoolean(xstring("rate_goal"), fieldRef(fn_rate))),
            (new QuText(xstring("gras_desc_instruction")))->addTag(tag),
            (new QuTextEdit(fieldRef(fn_desc)))->addTag(tag),
            (new QuText(xstring("gras_rate_instruction")))->addTag(tag),
            slider,
            (new QuSpacer())->addTag(tag),
        });
        connect(
            fieldRef(fn_rate).data(),
            &FieldRef::valueChanged,
            this,
            &GboGRaS::updateMandatory
        );
    };

    addGoal(1, FN_RATE_GOAL_1, FN_GOAL_1_DESC, FN_GOAL_1_PROGRESS);
    addGoal(2, FN_RATE_GOAL_2, FN_GOAL_2_DESC, FN_GOAL_2_PROGRESS);
    addGoal(3, FN_RATE_GOAL_3, FN_GOAL_3_DESC, FN_GOAL_3_PROGRESS);

    // ------------------------------------------------------------------------
    // Closing elements
    // ------------------------------------------------------------------------

    elements.append(
        {(new QuText(xstring("completed_by")))->setBold(true),
         (new QuMcq(fieldRef(FN_COMPLETED_BY), completed_by_options))
             ->setHorizontal(true)
             ->setAsTextButton(true),
         (new QuTextEdit(fieldRef(FN_COMPLETED_BY_OTHER), false))
             ->addTag(TAG_OTHER),

         new QuSpacer(),
         new QuHorizontalLine(),
         new QuSpacer(),
         (new QuText(xstring("copyright")))->setItalic()}
    );

    // ------------------------------------------------------------------------
    // Page, questionnaire, other setup
    // ------------------------------------------------------------------------

    m_page = new QuPage(elements);

    connect(
        fieldRef(FN_COMPLETED_BY).data(),
        &FieldRef::valueChanged,
        this,
        &GboGRaS::updateMandatory
    );

    m_page->setTitle(longname());

    m_questionnaire = new Questionnaire(m_app, {m_page});
    m_questionnaire->setReadOnly(read_only);

    updateMandatory();

    return m_questionnaire;
}

// ============================================================================
// Task-specific calculations
// ============================================================================

void GboGRaS::updateMandatory()
{
    const bool other = valueInt(FN_COMPLETED_BY) == gbocommon::AGENT_OTHER;
    fieldRef(FN_COMPLETED_BY_OTHER)->setMandatory(other);
    if (!m_questionnaire || !m_page) {
        return;
    }
    m_questionnaire->setVisibleByTag(TAG_OTHER, other);

    int n_goals = 0;
    auto doGoal
        = [this, &n_goals](const int goalnum, const QString& fn_rate) -> void {
        const QString tag = getGoalTag(goalnum);
        const bool rate = valueBool(fn_rate);
        m_questionnaire->setVisibleByTag(tag, rate);
        if (rate) {
            ++n_goals;
        }
    };
    doGoal(1, FN_RATE_GOAL_1);
    doGoal(2, FN_RATE_GOAL_2);
    doGoal(3, FN_RATE_GOAL_3);
    // We need at least one goal:
    m_page->blockProgress(n_goals == 0);
}