view main.cpp @ 0:fec4d0c461f2

Initial import of the post-prototyping phase code.
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 30 Mar 2017 03:20:08 +0300
parents
children db8f47446713
line wrap: on
line source

//
// Syntilista - velkalistasovellus Kampus-kahvilaan
// Programmed and designed by Matti Hämäläinen <ccr@tnsp.org>
// (C) Copyright 2017 Tecnic Software productions (TNSP)
//
#include <QApplication>
#include <QMessageBox>
#include <QSettings>
#include "main.h"
#include "ui_mainwindow.h"
#include "ui_editperson.h"

static QString query_Person =
    "SELECT id,last_name,first_name,"
    "(SELECT SUM(value) FROM transactions WHERE transactions.person=people.id) AS balance,"
    "updated FROM people";



void appError(QString title, QString msg)
{
    QMessageBox::critical(0, title, msg, QMessageBox::Ok);
    QApplication::exit();
}


double moneyStrToValue(const QString &str)
{
    QString str2 = str;
    return str2.replace(",", ".").toDouble();
}


QString moneyValueToStr(double val)
{
    return QStringLiteral("%1").arg(val, 1, 'f', 2);
}


QString cleanupStr(const QString &str)
{
    return str.simplified().trimmed();
}


const QString dateTimeToStr(const QDateTime &val)
{
    QDateTime tmp = val;
    tmp.setOffsetFromUtc(0);
    return tmp.toLocalTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"));
}


bool checkAndReportSQLError(const QString where, const QSqlError &err)
{
    if (err.isValid())
    {
        printf("SQL Error in %s: %s\n",
            where.toUtf8().constData(),
            err.text().toUtf8().constData());
        return true;
    }
    else
        return false;
}


int main(int argc, char *argv[])
{
    QApplication sapp(argc, argv);

    //
    // Initialize / open SQL database connection
    //
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName(qApp->applicationDirPath() + QDir::separator() + "syntilista.sqlite3");

    if (!db.open())
    {
        appError(
            "Cannot open database",
            "Unable to establish a database connection.\n"
            "This example needs SQLite support. Please read "
            "the Qt SQL driver documentation for information how "
            "to build it."
            );
        return false;
    }

    QSqlQuery query;
    query.exec(
        "CREATE TABLE people (id INTEGER PRIMARY KEY, "
        "first_name VARCHAR(128) NOT NULL, "
        "last_name VARCHAR(128) NOT NULL, "
        "extra_info VARCHAR(2048), "
        "added DATETIME NOT NULL, "
        "updated DATETIME NOT NULL)");

    checkAndReportSQLError("CREATE TABLE people", query.lastError());

    query.exec(
        "CREATE TABLE transactions ("
        "id INTEGER PRIMARY KEY, "
        "person INT NOT NULL, "
        "value REAL, "
        "added DATETIME NOT NULL)");

    checkAndReportSQLError("CREATE TABLE transactions", query.lastError());

    SyntilistaMainWindow swin;
    swin.show();
    return sapp.exec();
}


//
// Main application window code
//
SyntilistaMainWindow::SyntilistaMainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::SyntilistaMainWindow)
{
    ui->setupUi(this);
    ui->edit_Amount->setValidator(new QDoubleValidator(0, 1000, 2, this));

    readSettings();

    peopleSortIndex = 1;
    peopleSortOrder = Qt::AscendingOrder;
    peopleFilter = "";

    model_People = new PersonSQLModel();
    updatePersonList();

    ui->tableview_People->setModel(model_People);
    ui->tableview_People->setColumnHidden(0, true);
    ui->tableview_People->setItemDelegate(new QSqlRelationalDelegate(ui->tableview_People));
    ui->tableview_People->verticalHeader()->setVisible(false);
    ui->tableview_People->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    ui->tableview_People->setSortingEnabled(true);

    connect(ui->tableview_People->selectionModel(),
        SIGNAL(currentChanged(const QModelIndex &, const QModelIndex &)),
        this, SLOT(selectedPersonChanged(const QModelIndex &, const QModelIndex &)));

    connect(ui->tableview_People->horizontalHeader(),
        SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)),
        this,
        SLOT(updateSortOrder(int, Qt::SortOrder)));

    ui->tableview_People->horizontalHeader()->setSortIndicator(1, Qt::AscendingOrder);

    model_Latest = new TransactionSQLModel();
    ui->tableview_Latest->setModel(model_Latest);
    ui->tableview_Latest->setItemDelegate(new QSqlRelationalDelegate(ui->tableview_Latest));
    ui->tableview_Latest->verticalHeader()->setVisible(false);
    ui->tableview_Latest->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

    setActivePerson(-1);
}


SyntilistaMainWindow::~SyntilistaMainWindow()
{
    printf("QUITTAUS\n");

    saveSettings();

    delete ui;
    delete model_People;
    delete model_Latest;
}


void SyntilistaMainWindow::statusMsg(const QString &msg)
{
    ui->statusbar->showMessage(msg);
}


void SyntilistaMainWindow::readSettings()
{
    QSettings settings(APP_VENDOR, APP_NAME);
    move(settings.value("pos", QPoint(100, 100)).toPoint());
    resize(settings.value("size", QSize(1000, 600)).toSize());
}


void SyntilistaMainWindow::saveSettings()
{
    QSettings settings(APP_VENDOR, APP_NAME);
    settings.setValue("pos", pos());
    settings.setValue("size", size());
}


void SyntilistaMainWindow::selectedPersonChanged(const QModelIndex &curr, const QModelIndex &prev)
{
    (void) prev;
    int row = curr.row();
    if (row >= 0)
    {
        const QAbstractItemModel *model = curr.model();
        setActivePerson(model->data(model->index(row, 0)).toInt());
    }
    else
        setActivePerson(-1);
}


void SyntilistaMainWindow::updateSortOrder(int index, Qt::SortOrder order)
{
    peopleSortIndex = index;
    peopleSortOrder = order;
    updatePersonList();
}


void SyntilistaMainWindow::setActivePerson(qint64 id)
{
    personID = id;

    if (id >= 0)
    {
        QSqlQuery person;
        person.prepare(query_Person +" WHERE id=?");
        person.addBindValue(id);
        person.exec();
        checkAndReportSQLError("SELECT in setActivePerson()", person.lastError());

        if (!person.next())
        {
            statusMsg(tr("ERROR! No person with ID #%1").arg(id));
        }
        else
        {
            ui->personGB->setEnabled(true);
            ui->label_PersonName->setText(person.value(1).toString() +", "+ person.value(2).toString());

            double balance = person.value(3).toDouble();
            ui->label_BalanceValue->setText(moneyValueToStr(balance));
            ui->label_BalanceValue->setStyleSheet(balance < 0 ? "color: red;" : "color: green;");

            QSqlQuery query;
            query.prepare("SELECT id,value,added FROM transactions WHERE person=? ORDER BY added DESC LIMIT 5");
            query.addBindValue(id);
            query.exec();
            checkAndReportSQLError("SELECT transactions for tableview_Latest", query.lastError());

            model_Latest->setQuery(query);

            model_Latest->setHeaderData(0, Qt::Horizontal, "ID");
            model_Latest->setHeaderData(1, Qt::Horizontal, "Summa");
            model_Latest->setHeaderData(2, Qt::Horizontal, "Aika");

            ui->tableview_Latest->setModel(model_Latest);
            ui->tableview_Latest->setColumnHidden(0, true);
            ui->tableview_Latest->verticalHeader()->setVisible(false);
            ui->tableview_Latest->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

            return; // Ugly
        }
    }

    // In case of id < 0 or errors ..
    ui->personGB->setEnabled(false);
    ui->edit_Amount->clear();
    ui->label_BalanceValue->setText("--");
    ui->label_BalanceValue->setStyleSheet(NULL);
    ui->label_PersonName->setText("???");
    ui->tableview_Latest->setModel(NULL);
}


//
// Widget slot handlers
//
void SyntilistaMainWindow::on_button_Quit_clicked()
{
    close();
}


void SyntilistaMainWindow::on_button_AddPerson_clicked()
{
    EditPerson *person = new EditPerson(this);
    person->setPerson(-1);
}


void SyntilistaMainWindow::on_button_EditPerson_clicked()
{
    EditPerson *person = new EditPerson(this);
    person->setPerson(personID);
}


void SyntilistaMainWindow::on_tableview_People_doubleClicked(const QModelIndex &curr)
{
    int row = curr.row();
    if (row >= 0)
    {
        const QAbstractItemModel *model = curr.model();
        setActivePerson(model->data(model->index(row, 0)).toInt());

        EditPerson *person = new EditPerson(this);
        person->setPerson(personID);
    }
    else
        setActivePerson(-1);
}


void SyntilistaMainWindow::on_button_ClearFilter_clicked()
{
    ui->edit_PersonFilter->clear();
}


void SyntilistaMainWindow::updatePersonData(qint64 id)
{
    printf("updatePersonData(%lld)\n", id);
    if (id == personID)
        setActivePerson(id);
    
    model_People->updateModel();
}


void SyntilistaMainWindow::updatePersonList()
{
    QSqlQuery query;
    QString queryDir, querySort = QStringLiteral("");

    if (peopleSortOrder == Qt::AscendingOrder)
        queryDir = QStringLiteral("ASC");
    else
        queryDir = QStringLiteral("DESC");

    switch (peopleSortIndex)
    {
        case 1:
        case 2:
            querySort = QStringLiteral(" ORDER BY last_name ") + queryDir + QStringLiteral(",first_name ") + queryDir;
            break;
        
        case 3:
            querySort = QStringLiteral(" ORDER BY balance ") + queryDir;
            break;

        case 4:
            querySort = QStringLiteral(" ORDER BY updated ") + queryDir;
            break;
    }

    if (peopleFilter != "")
    {
        QString tmp = "%"+ peopleFilter +"%";
        query.prepare(query_Person +" WHERE first_name LIKE ? OR last_name LIKE ?" + querySort);

        query.addBindValue(tmp);
        query.addBindValue(tmp);
    }
    else
    {
        query.prepare(query_Person + querySort);
    }

    checkAndReportSQLError("updatePersonList() before exec", query.lastError());
    query.exec();
    checkAndReportSQLError("updatePersonList() after exec", query.lastError());

    model_People->setQuery(query);

    model_People->setHeaderData(0, Qt::Horizontal, "ID");
    model_People->setHeaderData(1, Qt::Horizontal, "Sukunimi");
    model_People->setHeaderData(2, Qt::Horizontal, "Etunimi");
    model_People->setHeaderData(3, Qt::Horizontal, "Tase");
    model_People->setHeaderData(4, Qt::Horizontal, "Muutettu");
}


void SyntilistaMainWindow::on_edit_PersonFilter_textChanged(const QString &str)
{
    peopleFilter = cleanupStr(str);
    updatePersonList();
}


void SyntilistaMainWindow::on_button_XXX_clicked()
{
//    printf("XXX-namiskaa painettu!\n");
}


bool SyntilistaMainWindow::addTransaction(bool debt, double value)
{
    if (personID <= 0)
        return false;

    QSqlQuery person;
    person.prepare("SELECT * FROM people WHERE id=?");
    person.addBindValue(personID);
    person.exec();
    person.next();

    if (value != 0)
    {
        QSqlQuery query;
        query.prepare("INSERT INTO transactions (person,value,added) VALUES (?,?,?)");
        query.addBindValue(personID);
        query.addBindValue(debt ? -value : value);
        query.addBindValue(QDateTime::currentDateTimeUtc());
        query.exec();
        checkAndReportSQLError("addTransaction()", query.lastError());

        query.prepare("UPDATE people SET updated=? WHERE id=?");
        query.addBindValue(QDateTime::currentDateTimeUtc());
        query.addBindValue(personID);
        query.exec();
        checkAndReportSQLError("addTransaction update timestamp", query.lastError());

        QSqlDatabase::database().commit();

        ui->edit_Amount->clear();
        updatePersonData(personID);

        QString str;
        if (debt)
        {
            str = tr("Lisättiin velkaa %1 EUR henkilölle '%2 %3' (#%4).").
                arg(value, 1, 'f', 2).
                arg(person.value(1).toString()).
                arg(person.value(2).toString()).
                arg(person.value(0).toInt());
        }
        else
        {
            str = tr("Vähennettiin velkaa %1 EUR henkilöltä '%2 %3' (#%4).").
                arg(value, 1, 'f', 2).
                arg(person.value(1).toString()).
                arg(person.value(2).toString()).
                arg(person.value(0).toInt());
        }


        statusMsg(str);
        return true;
    }
    else
    {
        QString tmp = (debt ? "lisätty" : "vähennetty");
        statusMsg("Velkaa ei "+ tmp +" koska summaa ei määritetty.");
        return false;
    }
}


void SyntilistaMainWindow::on_button_AddDebt_clicked()
{
    addTransaction(true, moneyStrToValue(ui->edit_Amount->text()));
}


void SyntilistaMainWindow::on_button_SubDebt_clicked()
{
    addTransaction(false, moneyStrToValue(ui->edit_Amount->text()));
}


//
// Edit person dialog
//
EditPerson::EditPerson(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::EditPerson)
{
    ui->setupUi(this);

    setModal(true);
    setAttribute(Qt::WA_DeleteOnClose);
    show();
    activateWindow();
    raise();
    setFocus();

    model_Transactions = new TransactionSQLModel();
    ui->tableview_Transactions->setModel(model_Transactions);
    ui->tableview_Transactions->setItemDelegate(new QSqlRelationalDelegate(ui->tableview_Transactions));
    ui->tableview_Transactions->verticalHeader()->setVisible(false);
    ui->tableview_Transactions->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

    validateForm();
}


EditPerson::~EditPerson()
{
    delete ui;
    delete model_Transactions;
}


void EditPerson::statusMsg(const QString &msg)
{
    dynamic_cast<SyntilistaMainWindow *>(parent())->statusMsg(msg);
}


bool EditPerson::validateForm(PersonInfo &info)
{
    info.firstName = cleanupStr(ui->edit_FirstName->text());
    info.lastName = cleanupStr(ui->edit_LastName->text());

    ui->edit_FirstName->setStyleSheet(info.firstName == "" ? "background-color: red;" : NULL);
    ui->edit_LastName->setStyleSheet(info.lastName == "" ? "background-color: red;" : NULL);

    return info.firstName != "" && info.lastName != "";
}


bool EditPerson::validateForm()
{
    PersonInfo info;
    return validateForm(info);
}


void EditPerson::on_button_Cancel_clicked()
{
    close();
}


void EditPerson::on_button_OK_clicked()
{
    PersonInfo info;
    info.id = personID;
    info.extraInfo = ui->textedit_ExtraInfo->document()->toPlainText();

    if (!validateForm(info))
        return;

    if (info.id >= 0)
    {
        QSqlQuery person;
        person.prepare("SELECT * FROM people WHERE id <> ? AND first_name=? AND last_name=?");
        person.addBindValue(info.id);
        person.addBindValue(info.firstName);
        person.addBindValue(info.lastName);
        person.exec();

        checkAndReportSQLError("SELECT check for existing person by same name (UPDATE)", person.lastError());
        
        if (person.next())
        {
            statusMsg(tr("Ei pysty! Samalla nimellä on jo henkilö!"));
            return;
        }

        dynamic_cast<SyntilistaMainWindow *>(parent())->model_People->updatePerson(QModelIndex(), info);

        statusMsg(tr("Päivitettiin henkilö '%1 %2' (#%3).").
            arg(info.firstName).arg(info.lastName).arg(info.id));
    }
    else
    {
        QSqlQuery person;
        person.prepare("SELECT * FROM people WHERE first_name=? AND last_name=?");
        person.addBindValue(info.firstName);
        person.addBindValue(info.lastName);
        person.exec();

        checkAndReportSQLError("SELECT check for existing person by same name (ADD)", person.lastError());

        if (person.next())
        {
            statusMsg(tr("Ei pysty! Samalla nimellä on jo henkilö!"));
            return;
        }

        dynamic_cast<SyntilistaMainWindow *>(parent())->model_People->addPerson(info);
        dynamic_cast<SyntilistaMainWindow *>(parent())->updatePersonList();

        statusMsg(tr("Lisättiin uusi henkilö '%1 %2'.").
            arg(info.firstName).arg(info.lastName));
    }

    close();
}


void EditPerson::on_edit_FirstName_textChanged(const QString &arg1)
{
    (void) arg1;
    validateForm();
}


void EditPerson::on_edit_LastName_textChanged(const QString &arg1)
{
    (void) arg1;
    validateForm();
}


void EditPerson::clearForm()
{
    ui->edit_FirstName->clear();
    ui->edit_LastName->clear();
    ui->textedit_ExtraInfo->document()->clear();
    ui->edit_FirstName->setFocus();
}


void EditPerson::setPerson(qint64 id)
{
    personID = id;

    if (id >= 0)
    {
        QSqlQuery person;
        person.prepare("SELECT * FROM people WHERE id=?");
        person.addBindValue(id);
        person.exec();
        checkAndReportSQLError("SELECT in EditPerson::setPerson()", person.lastError());

        if (!person.next())
        {
            statusMsg(tr("ERROR! No person with ID #%1").arg(id));
        }
        else
        {
            ui->edit_FirstName->setText(person.value(1).toString());
            ui->edit_LastName->setText(person.value(2).toString());
            ui->textedit_ExtraInfo->document()->setPlainText(person.value(3).toString());
            
            QSqlQuery query;
            query.prepare("SELECT id,value,added FROM transactions WHERE person=? ORDER BY added DESC");
            query.addBindValue(id);
            query.exec();
            checkAndReportSQLError("SELECT transactions for tableview_Transactions", query.lastError());

            model_Transactions->setQuery(query);

            model_Transactions->setHeaderData(0, Qt::Horizontal, "ID");
            model_Transactions->setHeaderData(1, Qt::Horizontal, "Summa");
            model_Transactions->setHeaderData(2, Qt::Horizontal, "Aika");

            ui->tableview_Transactions->setModel(model_Transactions);
            ui->tableview_Transactions->setColumnHidden(0, true);

            return; // Ugly
        }
    }

    // In case of id < 0 or errors ..
    clearForm();
    ui->tableview_Transactions->setModel(NULL);
}


//
// Custom SQL models
//
PersonSQLModel::PersonSQLModel(QObject *parent) : QSqlQueryModel(parent)
{
}


QVariant PersonSQLModel::data(const QModelIndex &index, int role) const
{
    QVariant value = QSqlQueryModel::data(index, role);

    if (value.isValid() && role == Qt::DisplayRole)
    {
        switch (index.column())
        {
            case 3:
                return moneyValueToStr(value.toDouble());

            case 4:
                return dateTimeToStr(value.toDateTime());
        }
    }

    if (index.column() == 3 && role == Qt::ForegroundRole)
    {
        double val = QSqlQueryModel::data(index, Qt::DisplayRole).toDouble();
        if (val < 0)
            return QVariant::fromValue(QColor(Qt::red));
        else
            return QVariant::fromValue(QColor(Qt::green));
    }

    return value;
}


void PersonSQLModel::updatePerson(const QModelIndex &item, const PersonInfo &person)
{
    QSqlQuery query;
    query.prepare("UPDATE people SET first_name=?,last_name=?,extra_info=?,updated=? WHERE id=?");
    query.addBindValue(person.firstName);
    query.addBindValue(person.lastName);
    query.addBindValue(person.extraInfo);
    query.addBindValue(QDateTime::currentDateTimeUtc());
    query.addBindValue(person.id);
    query.exec();

    checkAndReportSQLError("PersonSQLModel::updatePerson()", query.lastError());
    QSqlDatabase::database().commit();
    
    updateModel();
}


void PersonSQLModel::addPerson(const PersonInfo &person)
{
//    beginInsertRows(QModelIndex(), rowCount(), rowCount());

    QSqlQuery np;
    np.prepare("INSERT INTO people (first_name,last_name,extra_info,added,updated) VALUES (?,?,?,?,?)");
    np.addBindValue(person.firstName);
    np.addBindValue(person.lastName);
    np.addBindValue(person.extraInfo);
    np.addBindValue(QDateTime::currentDateTimeUtc());
    np.addBindValue(QDateTime::currentDateTimeUtc());
    np.exec();

    checkAndReportSQLError("PersonSQLModel::addPerson()", np.lastError());
    QSqlDatabase::database().commit();

//    endInsertRows();    
    updateModel();
}


void PersonSQLModel::updateModel()
{
    printf("PersonSQLModel::updateModelInfo()\n");
    query().exec();
    emit dataChanged(index(0, 0), index(rowCount(), columnCount()));
}


TransactionSQLModel::TransactionSQLModel(QObject *parent) : QSqlQueryModel(parent)
{
}


QVariant TransactionSQLModel::data(const QModelIndex &index, int role) const
{
    QVariant value = QSqlQueryModel::data(index, role);

    if (value.isValid() && role == Qt::DisplayRole)
    {
        switch (index.column())
        {
            case 1:
                return moneyValueToStr(value.toDouble());
            
            case 2:
                return dateTimeToStr(value.toDateTime());
        }
    }

    if (index.column() == 1 && role == Qt::ForegroundRole)
    {
        double val = QSqlQueryModel::data(index, Qt::DisplayRole).toDouble();
        if (val < 0)
            return QVariant::fromValue(QColor(Qt::red));
        else
            return QVariant::fromValue(QColor(Qt::green));
    }

    return value;
}


void TransactionSQLModel::updateModel()
{
    printf("TransactionSQLModel::updateModelInfo()\n");
    query().exec();
    emit dataChanged(QModelIndex(), QModelIndex());
}