From df3657911a53e60ca70775c5fadb033bb7d7abf5 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 <0xFEEDC0DE64@gmail.com> Date: Sat, 2 Dec 2017 10:50:30 +0100 Subject: [PATCH] Added existing sources --- .gitignore | 92 ++- dialogs/aboutmedialog.cpp | 21 + dialogs/aboutmedialog.h | 23 + dialogs/aboutmedialog.ui | 120 ++++ dialogs/authenticationdialog.cpp | 34 ++ dialogs/authenticationdialog.h | 28 + dialogs/authenticationdialog.ui | 133 +++++ dialogs/buchungdialog.cpp | 55 ++ dialogs/buchungdialog.h | 34 ++ dialogs/buchungdialog.ui | 139 +++++ dialogs/kontierungdialog.cpp | 88 +++ dialogs/kontierungdialog.h | 43 ++ dialogs/kontierungdialog.ui | 168 ++++++ eventloopwithstatus.cpp | 24 + eventloopwithstatus.h | 24 + images/authentication.png | Bin 0 -> 6495 bytes images/next.png | Bin 0 -> 4079 bytes images/previous.png | Bin 0 -> 4201 bytes images/quit.png | Bin 0 -> 3837 bytes images/refresh.png | Bin 0 -> 6914 bytes images/splash.png | Bin 0 -> 26172 bytes images/today.png | Bin 0 -> 5070 bytes main.cpp | 135 +++++ mainwindow.cpp | 978 +++++++++++++++++++++++++++++++ mainwindow.h | 57 ++ mainwindow.ui | 322 ++++++++++ models/buchungenmodel.cpp | 106 ++++ models/buchungenmodel.h | 68 +++ models/kontierungenmodel.cpp | 110 ++++ models/kontierungenmodel.h | 68 +++ resources.qrc | 11 + zeiterfassung.conf | 8 + zeiterfassung.cpp | 817 ++++++++++++++++++++++++++ zeiterfassung.h | 142 +++++ zeiterfassung.pro | 47 ++ 35 files changed, 3866 insertions(+), 29 deletions(-) create mode 100644 dialogs/aboutmedialog.cpp create mode 100644 dialogs/aboutmedialog.h create mode 100644 dialogs/aboutmedialog.ui create mode 100644 dialogs/authenticationdialog.cpp create mode 100644 dialogs/authenticationdialog.h create mode 100644 dialogs/authenticationdialog.ui create mode 100644 dialogs/buchungdialog.cpp create mode 100644 dialogs/buchungdialog.h create mode 100644 dialogs/buchungdialog.ui create mode 100644 dialogs/kontierungdialog.cpp create mode 100644 dialogs/kontierungdialog.h create mode 100644 dialogs/kontierungdialog.ui create mode 100644 eventloopwithstatus.cpp create mode 100644 eventloopwithstatus.h create mode 100644 images/authentication.png create mode 100644 images/next.png create mode 100644 images/previous.png create mode 100644 images/quit.png create mode 100644 images/refresh.png create mode 100644 images/splash.png create mode 100644 images/today.png create mode 100755 main.cpp create mode 100644 mainwindow.cpp create mode 100644 mainwindow.h create mode 100644 mainwindow.ui create mode 100644 models/buchungenmodel.cpp create mode 100644 models/buchungenmodel.h create mode 100644 models/kontierungenmodel.cpp create mode 100644 models/kontierungenmodel.h create mode 100644 resources.qrc create mode 100644 zeiterfassung.conf create mode 100644 zeiterfassung.cpp create mode 100644 zeiterfassung.h create mode 100755 zeiterfassung.pro diff --git a/.gitignore b/.gitignore index 6732e72..fab7372 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,73 @@ -# C++ objects and libs +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- -*.slo -*.lo -*.o +*~ +*.autosave *.a -*.la -*.lai +*.core +*.moc +*.o +*.obj +*.orig +*.rej *.so -*.dll -*.dylib - -# Qt-es - +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc /.qmake.cache /.qmake.stash -*.pro.user -*.pro.user.* -*.qbs.user -*.qbs.user.* -*.moc -moc_*.cpp -moc_*.h -qrc_*.cpp -ui_*.h -Makefile* -*build-* -# QtCreator +# qtcreator generated files +*.pro.user* -*.autosave +# xemacs temporary files +*.flc -# QtCtreator Qml -*.qmlproject.user -*.qmlproject.user.* +# Vim temporary files +.*.swp -# QtCtreator CMake -CMakeLists.txt.user* +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe diff --git a/dialogs/aboutmedialog.cpp b/dialogs/aboutmedialog.cpp new file mode 100644 index 0000000..57e00cb --- /dev/null +++ b/dialogs/aboutmedialog.cpp @@ -0,0 +1,21 @@ +#include "aboutmedialog.h" +#include "ui_aboutmedialog.h" + +AboutMeDialog::AboutMeDialog(const Zeiterfassung::UserInfo &userInfo, QWidget *parent) : + QDialog(parent), + ui(new Ui::AboutMeDialog), + m_userInfo(userInfo) +{ + ui->setupUi(this); + + ui->spinBoxUserId->setValue(userInfo.userId); + ui->lineEditEmail->setText(userInfo.email); + ui->lineEditLongUsername->setText(userInfo.longUsername); + ui->lineEditText->setText(userInfo.text); + ui->lineEditUsername->setText(userInfo.username); +} + +AboutMeDialog::~AboutMeDialog() +{ + delete ui; +} diff --git a/dialogs/aboutmedialog.h b/dialogs/aboutmedialog.h new file mode 100644 index 0000000..bbb76e1 --- /dev/null +++ b/dialogs/aboutmedialog.h @@ -0,0 +1,23 @@ +#ifndef ABOUTMEDIALOG_H +#define ABOUTMEDIALOG_H + +#include + +#include "zeiterfassung.h" + +namespace Ui { class AboutMeDialog; } + +class AboutMeDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AboutMeDialog(const Zeiterfassung::UserInfo &userInfo, QWidget *parent = 0); + ~AboutMeDialog(); + +private: + Ui::AboutMeDialog *ui; + const Zeiterfassung::UserInfo &m_userInfo; +}; + +#endif // ABOUTMEDIALOG_H diff --git a/dialogs/aboutmedialog.ui b/dialogs/aboutmedialog.ui new file mode 100644 index 0000000..07f8bcc --- /dev/null +++ b/dialogs/aboutmedialog.ui @@ -0,0 +1,120 @@ + + + AboutMeDialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + <h1>About me</h1> + + + + + + + + + User-ID: + + + spinBoxUserId + + + + + + + E-Mail: + + + lineEditEmail + + + + + + + Long username: + + + lineEditLongUsername + + + + + + + Text: + + + lineEditText + + + + + + + Username: + + + lineEditUsername + + + + + + + true + + + 16777215 + + + + + + + true + + + + + + + true + + + + + + + true + + + + + + + true + + + + + + + + + + diff --git a/dialogs/authenticationdialog.cpp b/dialogs/authenticationdialog.cpp new file mode 100644 index 0000000..5de7cf2 --- /dev/null +++ b/dialogs/authenticationdialog.cpp @@ -0,0 +1,34 @@ +#include "authenticationdialog.h" +#include "ui_authenticationdialog.h" + +AuthenticationDialog::AuthenticationDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::AuthenticationDialog) +{ + ui->setupUi(this); +} + +AuthenticationDialog::~AuthenticationDialog() +{ + delete ui; +} + +QString AuthenticationDialog::username() const +{ + return ui->lineEditUsername->text(); +} + +void AuthenticationDialog::setUsername(const QString &username) +{ + ui->lineEditUsername->setText(username); +} + +QString AuthenticationDialog::password() const +{ + return ui->lineEditPassword->text(); +} + +void AuthenticationDialog::setPassword(const QString &password) +{ + ui->lineEditPassword->setText(password); +} diff --git a/dialogs/authenticationdialog.h b/dialogs/authenticationdialog.h new file mode 100644 index 0000000..dc3c35c --- /dev/null +++ b/dialogs/authenticationdialog.h @@ -0,0 +1,28 @@ +#ifndef AUTHENTICATIONDIALOG_H +#define AUTHENTICATIONDIALOG_H + +#include + +namespace Ui { +class AuthenticationDialog; +} + +class AuthenticationDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AuthenticationDialog(QWidget *parent = 0); + ~AuthenticationDialog(); + + QString username() const; + void setUsername(const QString &username); + + QString password() const; + void setPassword(const QString &password); + +private: + Ui::AuthenticationDialog *ui; +}; + +#endif // AUTHENTICATIONDIALOG_H diff --git a/dialogs/authenticationdialog.ui b/dialogs/authenticationdialog.ui new file mode 100644 index 0000000..f1fd258 --- /dev/null +++ b/dialogs/authenticationdialog.ui @@ -0,0 +1,133 @@ + + + AuthenticationDialog + + + + 0 + 0 + 394 + 169 + + + + Dialog + + + + + + + + + 48 + 48 + + + + + 48 + 48 + + + + :/zeiterfassung/images/authentication.png + + + true + + + + + + + <h1>Authentication</h1> + + + + + + + + + + + Username: + + + lineEditUsername + + + + + + + Password: + + + lineEditPassword + + + + + + + + + + QLineEdit::Password + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + AuthenticationDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AuthenticationDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/dialogs/buchungdialog.cpp b/dialogs/buchungdialog.cpp new file mode 100644 index 0000000..a825205 --- /dev/null +++ b/dialogs/buchungdialog.cpp @@ -0,0 +1,55 @@ +#include "buchungdialog.h" +#include "ui_buchungdialog.h" + +BuchungDialog::BuchungDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::BuchungDialog) +{ + ui->setupUi(this); + ui->timeEditTime->setTime(QTime::currentTime()); +} + +BuchungDialog::~BuchungDialog() +{ + delete ui; +} + +QTime BuchungDialog::getTime() const +{ + return ui->timeEditTime->time(); +} + +void BuchungDialog::setTime(const QTime &time) +{ + ui->timeEditTime->setTime(time); +} + +QTime BuchungDialog::getTimespan() const +{ + return ui->timeEditTimespan->time(); +} + +void BuchungDialog::setTimespan(const QTime ×pan) +{ + ui->timeEditTimespan->setTime(timespan); +} + +QString BuchungDialog::getType() const +{ + return ui->comboBoxType->currentText(); +} + +void BuchungDialog::setType(const QString &type) +{ + ui->comboBoxType->setCurrentText(type); +} + +QString BuchungDialog::getText() const +{ + return ui->lineEditText->text(); +} + +void BuchungDialog::setText(const QString &text) +{ + ui->lineEditText->setText(text); +} diff --git a/dialogs/buchungdialog.h b/dialogs/buchungdialog.h new file mode 100644 index 0000000..fdee1ff --- /dev/null +++ b/dialogs/buchungdialog.h @@ -0,0 +1,34 @@ +#ifndef BUCHUNGDIALOG_H +#define BUCHUNGDIALOG_H + +#include + +namespace Ui { +class BuchungDialog; +} + +class BuchungDialog : public QDialog +{ + Q_OBJECT + +public: + explicit BuchungDialog(QWidget *parent = 0); + ~BuchungDialog(); + + QTime getTime() const; + void setTime(const QTime &time); + + QTime getTimespan() const; + void setTimespan(const QTime ×pan); + + QString getType() const; + void setType(const QString &type); + + QString getText() const; + void setText(const QString &text); + +private: + Ui::BuchungDialog *ui; +}; + +#endif // BUCHUNGDIALOG_H diff --git a/dialogs/buchungdialog.ui b/dialogs/buchungdialog.ui new file mode 100644 index 0000000..9dbe081 --- /dev/null +++ b/dialogs/buchungdialog.ui @@ -0,0 +1,139 @@ + + + BuchungDialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + <h1>Buchung</h1> + + + + + + + + + Time: + + + timeEditTime + + + + + + + Timespan: + + + timeEditTimespan + + + + + + + Type: + + + comboBoxType + + + + + + + Text: + + + lineEditText + + + + + + + HH:mm:ss + + + + + + + HH:mm:ss + + + + + + + + + + true + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + BuchungDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + BuchungDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/dialogs/kontierungdialog.cpp b/dialogs/kontierungdialog.cpp new file mode 100644 index 0000000..8899131 --- /dev/null +++ b/dialogs/kontierungdialog.cpp @@ -0,0 +1,88 @@ +#include "kontierungdialog.h" +#include "ui_kontierungdialog.h" + +#include +#include +#include + +KontierungDialog::KontierungDialog(Zeiterfassung &erfassung, const Zeiterfassung::UserInfo &userInfo, + const QVector &projekte, QWidget *parent) : + QDialog(parent), + ui(new Ui::KontierungDialog), + m_erfassung(erfassung), + m_userInfo(userInfo) +{ + ui->setupUi(this); + + for(const auto& projekt : projekte) + ui->comboBoxProjekt->addItem(projekt.label % " (" % projekt.value % ')', projekt.value); +} + +KontierungDialog::~KontierungDialog() +{ + delete ui; +} + +QTime KontierungDialog::getTime() const +{ + return ui->timeEditTime->time(); +} + +void KontierungDialog::setTime(const QTime &time) +{ + ui->timeEditTime->setTime(time); +} + +QTime KontierungDialog::getTimespan() const +{ + return ui->timeEditTimespan->time(); +} + +void KontierungDialog::setTimespan(const QTime ×pan) +{ + ui->timeEditTimespan->setTime(timespan); +} + +QString KontierungDialog::getProjekt() const +{ + return ui->comboBoxProjekt->currentData().toString(); +} + +void KontierungDialog::setProjekt(const QString &projekt) +{ + auto index = ui->comboBoxProjekt->findData(projekt); + if(index >= 0) + ui->comboBoxProjekt->setCurrentIndex(index); + else + qWarning() << "could not find projekt" << projekt; +} + +QString KontierungDialog::getSubprojekt() const +{ + return ui->lineEditSubprojekt->text(); +} + +void KontierungDialog::setSubprojekt(const QString &subprojekt) +{ + ui->lineEditSubprojekt->setText(subprojekt); +} + +QString KontierungDialog::getWorkpackage() const +{ + return ui->lineEditWorkpackage->text(); +} + +void KontierungDialog::setWorkpackage(const QString &workpackage) +{ + ui->lineEditWorkpackage->setText(workpackage); +} + +QString KontierungDialog::getText() const +{ + return ui->lineEditText->text(); +} + +void KontierungDialog::setText(const QString &text) +{ + ui->lineEditText->setText(text); +} diff --git a/dialogs/kontierungdialog.h b/dialogs/kontierungdialog.h new file mode 100644 index 0000000..5ed1ec4 --- /dev/null +++ b/dialogs/kontierungdialog.h @@ -0,0 +1,43 @@ +#ifndef KONTIERUNGDIALOG_H +#define KONTIERUNGDIALOG_H + +#include + +#include "zeiterfassung.h" + +namespace Ui { class KontierungDialog; } + +class KontierungDialog : public QDialog +{ + Q_OBJECT + +public: + explicit KontierungDialog(Zeiterfassung &erfassung, const Zeiterfassung::UserInfo &userInfo, + const QVector &projekte, QWidget *parent = 0); + ~KontierungDialog(); + + QTime getTime() const; + void setTime(const QTime &time); + + QTime getTimespan() const; + void setTimespan(const QTime ×pan); + + QString getProjekt() const; + void setProjekt(const QString &projekt); + + QString getSubprojekt() const; + void setSubprojekt(const QString &subprojekt); + + QString getWorkpackage() const; + void setWorkpackage(const QString &workpackage); + + QString getText() const; + void setText(const QString &text); + +private: + Ui::KontierungDialog *ui; + Zeiterfassung &m_erfassung; + const Zeiterfassung::UserInfo &m_userInfo; +}; + +#endif // KONTIERUNGDIALOG_H diff --git a/dialogs/kontierungdialog.ui b/dialogs/kontierungdialog.ui new file mode 100644 index 0000000..6a3ba6a --- /dev/null +++ b/dialogs/kontierungdialog.ui @@ -0,0 +1,168 @@ + + + KontierungDialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + <h1>Kontierung</h1> + + + + + + + + + Time: + + + timeEditTime + + + + + + + Timespan: + + + timeEditTimespan + + + + + + + Projekt: + + + comboBoxProjekt + + + + + + + Subprojekt: + + + lineEditSubprojekt + + + + + + + Workpackage: + + + lineEditWorkpackage + + + + + + + Text: + + + lineEditText + + + + + + + HH:mm:ss + + + + + + + HH:mm:ss + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + KontierungDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + KontierungDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/eventloopwithstatus.cpp b/eventloopwithstatus.cpp new file mode 100644 index 0000000..ea70956 --- /dev/null +++ b/eventloopwithstatus.cpp @@ -0,0 +1,24 @@ +#include "eventloopwithstatus.h" + +EventLoopWithStatus::EventLoopWithStatus(QObject *parent) : + QEventLoop(parent) +{ + +} + +bool EventLoopWithStatus::success() const +{ + return m_success; +} + +const QString &EventLoopWithStatus::message() const +{ + return m_message; +} + +void EventLoopWithStatus::quitWithStatus(bool success, const QString &message) +{ + m_success = success; + m_message = message; + quit(); +} diff --git a/eventloopwithstatus.h b/eventloopwithstatus.h new file mode 100644 index 0000000..ecdcbb0 --- /dev/null +++ b/eventloopwithstatus.h @@ -0,0 +1,24 @@ +#ifndef EVENTLOOPWITHSTATUS_H +#define EVENTLOOPWITHSTATUS_H + +#include + +class EventLoopWithStatus : public QEventLoop +{ + Q_OBJECT + +public: + EventLoopWithStatus(QObject *parent = Q_NULLPTR); + + bool success() const; + const QString &message() const; + +public Q_SLOTS: + void quitWithStatus(bool success, const QString &message); + +private: + bool m_success; + QString m_message; +}; + +#endif // EVENTLOOPWITHSTATUS_H diff --git a/images/authentication.png b/images/authentication.png new file mode 100644 index 0000000000000000000000000000000000000000..4685f3badabcf5e6d8d32dea3638afcb947b90f0 GIT binary patch literal 6495 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↦oqu+d%=MR3D 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 bYTZnbYOpuno3AbP0l+XkK-&;_R literal 0 HcmV?d00001 diff --git a/images/next.png b/images/next.png new file mode 100644 index 0000000000000000000000000000000000000000..0df2a6ceafaf7528b57c0e0090ebbfb3f3988ef8 GIT binary patch literal 4079 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hEaJuGt~XX6F6+*w+pfx)XhR;*B1pi%0Z zV1Lu0w~bF0r=RG0{M?O|zaBlP z^3MJ?)B5?n;(ht`ucNc)TOZh2m^VlI|9p1S-%LtQ|Ezqjd^!F1n}CASK@Zb^B@#DP zqs!!$rklNc{qO6#gY(zy{hU?2%rU(^OQ_<{>%-Aj^CPzZ-oNO^Jsl-;S=m+V>h|Vm z9CnSh-}l3(InRob;mhRL<$t~JKYV!Yv2v)7mUf7nd!p|T&Sx*4R(8wnS(tpW|D4IU zWkLt4`T1gQe4hQ6Be{9z=4Fqh>%V!%Sk|jq@I@bS(Y|qv|9te0MQ_)rG<2kF33B}Y z%-kULn8=&e3^NP_x9jW``d@KbRO6{c(qVn$`?BI}Mux-&3rSb6Z6>5*xI#|0l* z9J-LvVLAQGu}|-Hj~svI%QpK~&c=`W&r9@n=e{%j{(5sygFudx%SKn3*(QfH+(KD* ze3cG=y!v_^pXx+0;lIMSf84a3ds@#Tn`1%?LvmtIqv5i~j$~y;v6@%PJDyv|ANnb; z<(%`Q{m`~_zvr7;(^(mFlsh;D(?li+Oms2rX)NNr|9iLoqwe}of*qW!M{>7d^2RT>PG9(WT%N7*eDjCPvaze*y?7Bd zH(QEPiAnHer-y>l`^E;2oJVY|ChN=&rzzVm_iHlkIVfxzW0P<6uk7S;!@YY~xIB5U z_{qur`l_AFn;QzB6+Q3T%Dvoo-j1AD|My?icW>VrrrN4Lr22B(EC$P z+gpj>-}8~%!oq+jt>I`Z#{rMNr5{TfZ>*0fI&<=naPQ<h_gbXejrrJ;2FT%I2{CZ5=5cfGUVIFsKA0mjV^Es9DSPno~Su6o${^|xSjC+C+~ zfxA14&zAOVw>%ckC}?23YAeUFh$FmPX9pkO_9k`qsat1rmg*JHp1S9N(XkfhDYf#^ zA74I7OIGz|PnPN}-Vt>8NZ7&BdMoM8%cd166|uX_dl#|i)1GqQh7Q&v!UET4Pf;=V zopk=+u}L#?*^b=q`t`g=RZ-QPcjfPvt(o65{@NH>b)Ao~ldLazDbn#FlXCIYgRuzXRfD`K^-AC0oLau%B1aG7 zY^jC(j?O8L<^sy4OqWj?wy|Vyj!oIy$)(&JRlyV9O&+9;{h4FH>}}ads_0(&Ykgf502fNf6w5`g>fip_vymB|M+t|ZeEdFo9=I&!^ny^UphFGnDMkSwwF!l5q591cgG7Yr9>E!^7n?d_(Z zP&?m3BbpNj3ANNbv1PiM)z5aago$};3d)5^TwzOTk#duvRYWMZhUGWRtR0WJy zDy-CUVevWl!=Y~b)447#SG8L;IJ7rB+I2^i@lEK2C$g_E@u0)v{%dqW$; zE}S|Yr~crz<^rK&ug`|De@e2w_pv{?Vk?=zs!`6@scgIZ(}^cmrfeE$Du{Rgqf zB5E&AO*b%~Vpz_6u0iO7)?$vO$D|XlG1Pwh{U=?ZKeE=psl!2}Q6+ns*5gn29x{qV zb7@RrdUI>1H1qK`h84#hUTL@QdT;eD>_MYpb))$&_Z2aE3-<2i<%wT5Be34$Rd|T% zONU;q3tt0XZp^DWJ9){}Z%a~}4gNWLAM=^)qVAB{x-E0ELdVC|zUv>nc({1+p_&vZ8ojg<8d)|4T2|^l*9$~LIfBs8fIK_RVY5VnUxrZBVe=924 zx>R}o|CPFaU+e>QRfWE(bE^Z7UpzH2Ik>$n@94a{ziap%GWQ7{S=w1&z_jt5W`Wi3 z{Tq*d;Xi+D()zo7H_{w`{qg5*WGtMo+od2^lU24NTxcoB)0M)8Mn(-^&;QfcI;_le zVegVJYun9r`0s1}C}h!@5xC;s<@z7b?(W_FuD14f_U1CPCLcOM@Rn16g>4fv7>I=Jo7slitS0!+;P>Z##P(%+D*R+ELdm1?dihh_jo@{ z7J4wV?n~lCck#W)|G(7s*&ia1vBH7TDmm|@a>{|7`-^t_3b87!{QmLZFPE02Xh~D! zo_AaQ6E^&pVEDtdWE$TWo02ECcUAnt*ckVOf0*Ytfy-fE@R@aXsoRvBQm)U4EUjNq zBz2U3Ytr#+zCW&hzH1;D-uY#_C;NZ13!8u4ta{?2@MjcLkXQ3SNCl z{ycYzQ=Xf4Rr!~M!;kHqbC+l>X?j2T(DwFiwn5f+%WuZ8YVdyI4f@2(|6uQeMDgcp zT2BS1OX}NgliF>T+s45s%NX#(>^*hFCNAnXM_lbM z&4^@YrCRNk5AIF8m+senajqm&hr+3;4ogK7cIK58%&j+B*|PWW?~iw4PZzBD&MS~% z?V-Q&P360(Y0UXnj7$tC+d4&N+(|BJJdkndqh@MOO_}btH9!4lWQ8)6?$`Lb_SL<2 z7r%4Y1ao@u_^o`BIpH;<+iAvcvm9-z-+wPG_$=n|M8l(Y>8Q~hLU(D%DHJ3RA1by~C+5BVfOyiB>qEpl} z-`HPhpB}cx>He`^foB{G-ZxyiyLI1JU-Mnx=Qiw1T=7|N&w(vIv&G)r+9mk+{Qicw zcl8e)6IA#jEuhQEpL3%~tx@-!AGJv@=f`|`E?V`m&uObsZN`HiVnJE1x%YpS|FO4uehl2bdhRBC{6FI!?py!% z%6HyiU|>)!ag8WRE=o--N!3kBEJb88S*r3as??!FFYqr6%X+=H}-W>*W`v>*qj} z6zeBd>J}&J8|xY9E94g`5TnD#J-|obNY79|y5A-l=0=F~kV3)0Sl7@p#L&RX)X2)z zNY}v3%D}*{;rVO^1_oh}8-w#pi;`0r{3|NcQ}Yx&lk@Y6-$<-`!N9gn_T%XlB3x7=1`rGiJ z!B9*UHmQijb#Ii@KDqnW@i*trJ-+wu->1dD?b1{m1)|os zUJcz_`+n`r>fdwh?l0bN%{?dk`K_I0>;6ia&w9z}lN-M$e>eM~S=x3M&mXP(Jgfg2 z^WXL6ad&N9Ol*Tijd?BZ+Fkg4cIoXnt1mU@#P7e0yzt3f{7m-GSJ}VaIG|OUsy`VPnxz3%++geX~#m07)j`d z2hU%^Q8$a*yFp;)4L;2Zw5$7!rEEGU_~8Z^wA_ zo}K)I-3OoXH5~2Tzmws5%(d2!SqvHMmT^BdU6w?-Ff&hZNswjuqwVLgvHY@-f&IA$ zQ)OA@_y7NR^u%*p9{&&rm)-}DKfk!mq9pU2;UJG%(?!J-FD}oMzAarnt+XY$eCqp| z4N32pMIZbVnx+@CZ;PyX%+*#efki>aVm7J|CMS1oNM6Nt$wok84YPsgmf8Z-$sZgJ zF}JW!vU{_Wf0CTSn@S%!o&!DH>-IC7*L@LMv!~?agAX^3JZJoKTT*esjom9*d)R0E zyf()_;_pM&8TV&8uZ-F1!0I@mgE89Z!Ikrxu5)sp%E+#9NO-L@Lne_Wi7|=EkwM4d z!Z(R)^97=vvrc)%)CHPvlC=DOmU;gVF5~~zRq=&UmT84|mrZ4`KX=A^mv!-vx2vrd zMn(s?CUvXp4QXK%oj(kZ*1An!0O1+5Yceb!br!@n<4s4 z&Ou>gjui3j2ievr+ZYASW4ch9obc%44a2LGi+SU@YX0A}H8?iwY359a!n?W}=On0^V?MmE` zD3K8&tA#BJ zjEZmfdU|ME9!xM`k#RUE&^CuFCoA7I;q1Y##z$AT8>z2mOm^p%G0014W1jQwnBc>( zcQH$1m<;plnLod+Ilg7l3|IF?gUGdxUYY8uZVJc4HPhgT(aTOPmrftw{X3cJE}?Gt=gDun3pCH>718Hsp+o7Fr@O?OeID!}(pu z^^!hpKd>jqud(nYt6YauNOpt3p(c@r2`mRw4)bX^<=bxDkiZ!6!RK4BOXqR6Ib2e? z#)^$|oShf3xVOEPnY=(*RHE$~C*!;+(-?}mSVXSpwj}s}n#%`0Tt&d%6Y=;cv9TTj|= zHRFY27Df`cBr4{yOq_i7w%~>*)7-6k_{;?=e2d){Tl8O?!`ft(WG(7s^`W6;wOr!^ zj#l1Vg>A?CW@Iew)&QP(9J!c&o% zl5=HsXV~vfQ2pemcED|SXxfT^gC<9O*d}I7Fm`8|z*sViqgs=%Qu9Ng*6bDrLDg*= z{X19%Q)?Q6XYk6td3Vs}sy;(&4d2{c-w&#ZH8n!(|GhETmQ&4nN&Nj9h7GSm7ydlA zFmYpnpQBTF+r*O<8F}ZJcqSw;gcV(TT)gKKXTj^ACwFA|-AYt6Y}F9%3VyV5wu(w6 z!$FICLHpVobKDk9IK61;&GsO!t6sA1{Zl`j`TSSHCd^5KEg-QWz<87IJ!|WO+xLBM zogcH|*Sx1&tA2bv%y`W#olC-ggS>FE>XXmD8SgV(y%WXK&Yn`9pWilJluu=WY4ZsO zY01e679ZOWEquNuxZB+8P*}~2AGL|cn2j#EUsF%9WlU=el5$)Spmp8QAmUiJv+#ZRCSZK=9?KdUyj6j-L}AN{41s{L2+2tnSGEK#6qN0w1f7OI}4I)BLE`EJ?uZ2dtHXPJRi3|-28m&;qSPhCm!XFD?MGF z2VYUl-QKl9?a}XR=|A?x*1p_1wSWKhiuWf(G`DiRT;x+Dqj_MC8Y4qN$#2`kIakhF z@z|ReJSa$gaDaj9xv6q|hTo|;kEv^xwQOIaFv(}bK@%Iv$q7r}H@I)|(>^=(`Xa03 z);8#_wD8&&{)UI_j;* z_EIN8b$<4PUpqE^pOQWCdHUm=d5T4H@Am)pH#D?)^ofmANJxQ`gHg%y>8`s8hLb)` zeC`=j{MKtr$vMaD=-UV7zU(PyVn}$=%3I1%UwJQeepS_n2M_aodV0?NKJfIk!S>D4 zSw^cKtN;G4v9UM1>Yc@^dl|X=on1U#+c}?JH0+U+Wto%g>e9e0&#W%}OCn{py{1sv zzYTpqxp(o&S(QB5oc?F;fe-6?{mXUsmqw;8`txGmTkG)i?=73U!(vz^tJYb~)=Rmi zyVCRR)mFm*zCE2@i8+t$jc?Af7kt&MKmXf$6}!Fj3&oY@U$}AZMD+2AcWa#$K3ZGx z{QvVnJ3nN*{a4$bt9;6rXE7^l#vNN6UAS1cVy(%cfSq!;-uP**Sh4GuT>ZHp+b2%8 zakNfeAM0Ls>-gg8{k8VS>*q+w-Q4~8*H~uUmsrIo?=}Uw`Pi$xy+sKMRDhup6SH#2;MR`?}+PD znEG2}Q zL;vuTKNVk{r{1Vr;KXu{y(Mu;!}oX0X*~=K8sGD6y@Kv#^!)5z{5x)Ce(eXlH`YId zn!d?6B`3}1KeUB^!*!?JJLi+nB{9ix+Wh?d|0g@!x>bFBfA4eG&i-&Fc(%cxzLW0GpgCM;rud< z4}NQ_5~`P06&!!gxND_N!IK;3*!MNfJ#W|9c}~X3b7hRemYa#rYd_RFIQSe{DC=)@ zu_$zX{@eS1=KpxuR$XWj-1I8LZ9#6{f}TRr&zp64=4AyxZdcoLt+h5`-ip4Snw$qq z#Z$L-UM%_;RFS*ZV6EhoZ*LF%xL|PT(!os5W@E1gWyY+eg!XWY`puk8uifJ7bH80! zz5C+E3_T{Uhf|Lk*jcPmWB>hrUf=WcKkR*6*4F*_8OyLX;ezwli-mHxH?uDbYMo;% z?vwPr#J2pH=!CpoHUh6Y8>T+M6-aG5OezT`n)q(qUbF<&? z{`UV{;jwAD51bb;Ffgc=xJHyD7o{ear0S+5mZUNm85mgV8XD>vnuZvfTNxNx85`>w zm|Gbb6gzB~1JeLfX$8^6U}Ruuple{HYh(~&WNc+*W@Ts!(ZFUM(}3a14BBez3>hUQ1y=g{V7oHQQj_y@bMy0x z_413-^>ZLfiuIE!b&C`Ajr9!l74nM|h|%HW9^j*Iq-Urf-EWf&b0frgNTFb0tZQf) zVrXDxYGh?Ca`m#P&&ZI z@j#`ok)e@Up1(|vDcQVZZSxY7!_HS+yM#3PJkJ*Nty^dF<>0)u?>_vNd@?EA8ouSF zn|JNIe}8-3{#`0B{DtEFMSGq)U7fdu;TxlKU28qFCxcJ~y%9hxLz7UAosEy!xt>mt=pC zN_Ui|>XIqpjq;4%4L7tIxeOZC%h=cL`Eg7-KVbFMh`6|8+j5so@z5-kS^oCc{_pE$ zk2`9%P7zufQXoHF_IP4l-Mp<^xArAGp698#GDPdUm*#u+e+}1v>{VmzIC#+U-R}2x ztFCIbC0@wd$|Q6srKMO#@S=>WgI8y20!i!_EFRQFzNkH0e~q8%mYqnR!*0_^FpY`wR^(+~)u1%X7bTHp!x#HzQnHB--f7`;DtfqeNWl&^k zOI+~UYS%jUzmeBVr+?Bruz1>6U3u2^nNjXclMWm!oxV@_LGyJJU50nf9ru>!7F^N% z^{VS%?5w;G;fe>s9d9%xez^OJLtDX*vyjWf*&(R%{%(gE4O^!5pR&Bg`{(r*7L~@{ z;DgUEu^wV^3BJRic4vjfdik)%`>gv4xIWlF`2OF5>5)w0mWP3=Eu1&A#XG!La{g!y zH-C3y|1Z6edtQz%cQ$X}nq*TM!db{t^6R(X~DD0{o(NT2W89qf4D5^ z{q5PyGg-(y!Hau`JWGVrWj^WFLyybWs{M;SCI5H1^t+P7G%cw4KL z4lwc@4q%^O6SP`S@89QzCp}*mF1`0FxTD2_VebS%-$J|O?R5+6{Q@-E_~o9=TVH>7 z%GztM*T?U%u;M;klHnmE`SA6J4VGM1UJm<37hFsZYM3->Av3$hr%T?)>}`HLFkXMa zo{yW6^HTq$i=8vJSn{m6{5fymqI9Mj(Jj-ao!~w=aeADQpRCms+se06=ic6)VBmB2 z?ccogNsGD-%e)(yHKrU|7q)V)t#{n@pQpa>JAUwLXxP0P;osMETg3laMXJ254f0C< zsv_Dl`Qg#;iVs%vJqbBfHIK#pKu1tkge9LwYwdHstDDm`Wvq`e>+d;m=6&7qS1&Jr z_;kACprgx*|L@#b+Y$pbL^8}~U&-2f!$4yBHjO~f^9i>(?Uv4bHv8wtk4#W+IZ&D)N9RZ80Ll6G=Ve$C! z<14RA!@|RNmcAC7J$v?F)dSHA@+W(H-c&!?dfn*S1BFxDVkI@U`nqHoDg|pOOrOTY zpPQc-WL-b6aj|>li=EGB{QUcV<-bqUJHq2#MY;uMUHNk9``?QX4jf>ZmTG8VaNz0b z>DGNL<*ON%W=h>`WZY3X`P!#XeZ0*M{9YSbEuy1WTqwAD@5TZ%%aaGD??1zQ-tP39 z>i2J4XL3j34PR_o*&d|*4*pVYF%F4=HqI7u<=WdfXvlhJYHuAQ` zfsMIOUTK{@>MC%OPa~xG_S(H09doN@9dws_*tmRt+PA~}EC2qw-Vq+h=+C)#)q!of zdNDg3Hby*oxBLCE_@ev0Opo5(UexgE*H)$}0(XtKq%YlY{rbw-9S0u7mKXk5x%`B` zzw*60>A;l=dmMetA3XdWqj&r;zx|oL-|utYX^oV}r0Xnx=>X8@O~YF*EL#rJWWgSK3|c4OAur4q$$#>FiQ(RygXH`9mB_wt5Z>uCla<_cZY*javT zwzs&m_zDJ<#nv+Pbf3r_a`?rB|~O zXDtg~>a`^+X8J3(eX)!#yByBUipg*Le&GH6^Ikj8d^Jwbm}ArV`BC?T(~c)51quXt znVFdt$f)07`C2~9s#kK-fijf?+CmrgHbk>(ymvnKLgU!KziXzQj&J9i!ohQaTvqmm- zb)vq`j&ngAJr5JlY&5*~>{-ODSxJvS9$#tyTPMn_``KRM>6gAdy(=K>?vNx>@F48- zxw%VE%h#GHv;@d$>Q(&Ln44Yc+y3WY^6@i|epCn;o_UyfX7{_K+`K;N%a_;4+xxwJ zx%{F{yEyy4$$P9Dy;pasFg-fPcq{$G$L64a3$|_y%xJjBwD{@YTMKT#+d8lDP$O{NE&g}rqJ-2T8QIj_x_Pt3^*4PzA{?^+tFDWkJWt6aK2r`wA^(~;U-yZ#uDHB` z^UulM=6mCni`L6GJ~E8>F1yswq-AHm{8aDhJ0i6n);zb)IB(y7En`9DfyKLb?)BJn zxpw{C1;UJc>(2bGbS#Ne-uv8(J-TA`p+lPtzP(MZO>ESDey;HIi@q`O+>_&WhnyUqZm}ar^^2*d7+fnyY$b8wdS@K(= z)>7XArJL=SPYDK4HSEVz3XlOr}t;lr5AjPD>Y<;}@wb#+v zy~ob~XTP`p;Qp5jcUH+TCVt3V^KPDQz?wBW#@6vkKaUA*%UvwVq*ybbiD_f4Y*@?p zGx2vtIeMnu5u7l|%)M60!muUoj4rTCTHG$ z^XBNuo$PB%w5CR%IpbUO^_8kr@2=?eODhi~nRz&+sMH?Mu;jRCx8e0R>o@+KOPTx* zFJr4``4@hm|9+e91BM4Lb}39YY;F$p{M@;O?bR;FW0zKhU7s=6`t+SYCRbm{wlEk! zj*i(HJ^Mr3n{7w8pT506dxqJPkOO^R#0=Pa+p3+8{M0mI;I7}l;c$iZosPHr-kTmU zFyNW&R@}Vvr}30i^Ed5#C}(46tR!a9vgUo5jN&zM+eT*rrv=h{P4`3umb}l8&pX=W z9oc_BAlCfEQ==P2-L0qV(_S4?U$aJU(Ucw$6-p+&+S&p+`SaM`em~gSSYedjewe$>NwJ7Y z|AF{o>6eES-fKo}ObpQAnQ%ICO@7?NGt1u_TL0(VI?K9i^CqT(ZPGGKQt@sb0s$=| zkM>^h3kYz$u;+Xz<3Dy&-{gvn88wRTrLWo2m)WPJy;C+bo9^gv?2^-81DRE~*57t$ zIk<4!(s%!N|2^Ml^+(-_!{qKi8PNx|TC>ky%DMaO(VOZi&+et?{^eS5BBE}^q%9{c z4lUX(B`9}b%YKD2F*lC@mmAz17kDM3UcS(0bmCZH9=@~gv9MS1`%SyTjuqx?vedn@ z;ewI(?uUm09#!OqH^+D9KXeFC+9;)Xpx`*8(jPZ_j)I1@H+Odb`F{Ri^VO}HIg#!h z@)b`?D%uMaC&dY@{I^HB*va8*pY!IGDgp1s8SM`BtDiq_V_<4&tW`G4)Rb|4y3mi` z`?I)ae2U%_&Xf~l_WyjL2D9b%AKgFX-JAbg?rPl46n5P9=eFllm_OAtPIUM95W2fF z`@nzy`s*+CTjt;U9KZRVCj$e6YKdz^NpewYVo9oQN@7VWgOP!OrLLiou7PEUfw`5j znU$f5u7SCgfkBm+0jMJe)&Npz1<}P|WMF8ZYhb8rXc%H(X=P|;Wnc=?aP8R@O_&W} z4Y~O#nQ4`{H7wtqGnIjX!35+apUmXcyy8@bjFOT9D}8;iU72O6$@#gt`FX{9`9>}VL)#Yv_3=9mCAPa-@(@M${i&7cN%ggmL^RkPR z6AMy7_RM}~BgMeLAPiAdT9lm1;O^(|>#FOVpOaryylSS)BL)TrLxgmEXpj%w-T5i0 rdg=N3={c!-$@#hZ1^VfE`MIgO$zZ+u0kT_PgSxSvu6{1-oD!M5oB~f~#Qr zA_Iwb3!aIs8=Ey*jD%)gowfVw-B%C4RkRzw+j~7KzV1!&@_Evn|0=@QM*W^w{Lb!m z?f&o5*9soY|EaqFm3}~|OG|(K_s8FE{MqQ0G&wD0k(+kz(v7{^N=lB-*G^hR3U>8z z=Ox~~vFqUS{GVHx1%6)T`xyS|%Pm<`E?pn(tX`8x$DX;}+OcZtwskq7(X)@QN`CFW zdU5jNs~5di-}35_c$s#$W}p51v)|I6Kl}Z;{Tch`-(qas{w1Hz9sav$fBM3R8NI%5 z{Qo=`*8df;Sp2Q0JeyF0!)EgrZw<2YUb+8$9ehdKCudn!;98^5*-3v-R9)*`lf23D zWJak-w$5C~_Q!J{%gnPslUOYATt@co6W2hQ_g}2ffBAIJf6gxXPx+NC9hNp9TE)|E z=*KYDGhK18^2vDM##uoEV#$6xX%Fkc zYX3lyFYL8_XXTU6PqckwwWB+>y{s_fUqHb@+2fzMdG0&?XmmOA!14c=Kh8Dpzvn;C z_#g4<7;k>f*JIC)o!#7gb9emae_Ma=`}re&9|MzJra^9N#4MJ*p?mkm+g?{y%gmj3 zJ_U|?) z_Ex`3{l4*TisI|ehjtr3h4S2=cxDcdX@A|nGlxy)3p>Tnwl>TAA71}wQ~gi%X!q@Y z@2&Q|UR^G~=l5Z0sn@^vi!ff4Wm^-)RoPng&-cSa*_FQTIe0NifB)ByQ08n=>N8U(r&q(-OKyzJO%dO zcX{S-_=lhK%r2J?^Glcf*JN7S`H(OFkGpwqURKSct@EEee9ZrGukHQ2wQI`XOgnOH zw|+&-#dhIGhZeh;t@`ku`62)8scEl|z1@FOdi#FP=*|rB+&#a7c}A&Wx08f?qZ-$lsq*6#Qjd_+PVVv%ICTTQsYVuIKr2>hxpF z$T=!Tg4f?w$WHf(Jyb6DWA?3iH*NnrP2pX(N`AV+?$9rMybl*=&$`@Oo?p+O{rdHc z*XJKLAC_tsWR7tVTT!oY@HwOKNx#U!MBu+x5MZy*7KPzhCfTJ?o!S*@g{f zkJ$183Z5Un|9IkI{)KMuC!Vlbcx-?6Cd;e8=FWYe=9_JMcM| z)wZAMmAuEo^%2?I0zX&^=Iu*+t-a&v*L+2lQ#twHu5(pgRc$!5X0~dt)}3#k$^+J4 z*;)SIe($-~N4H)cld8P^t=uSZe%dMuv*hg4C7Dxq z-n;Wjc}Y!=XtSWBB|GEV@5>ehg|9Ccs5qV~z5OKb`hUJvUoS=$tM!a%BC^5^BWZBvFFzdFZ$fvTW{X~zHUyl zJO8EgzcMZu3*XS+t*qYn;mzmsLdy*wCqK^Fo1z_Umb>s~j-K$wOWt3m{LA^y)iQ}e z*_r8_1aqIZ+SFZqUz#61>C)D}fAYvK&0EdiSQ)n`dHSAe+{?b>|Hj{z1z#A>?M}^} zx%tmxp4qIXUUEfgY3UZX>`n!<&wKP~?exv5m&I01ef6@?>HLNH4^0J+W~^iow7koz zR=3};aLEC)xA~dt-p`SloAx?&`bABv=lbcQk2KuaPuce0J-qn5o!7MV(+9)~S0ylc z7@R05&{`12xp7gMg;ww+L7xXM?^*2YKRuV3VS6C-_PV5`lt*t^=2Y$fWEvULVA;+6 zWon9z+}FP!+QSxq%$=U=u07%T^N-c7CSH7Oxo^`73UXxl8Lok*1GK zj$wigjRKkrg$jBb*0Y;P3MC%sPXB-M?-u#|;lLD;YM=YA~(8 zpkV!CgKzO(v8umMZm1-`R(p}U_p?NtXRcTIy3_sd{~8*rr-e-vWp$nvos)Qgv5~L9 zVQMXhtb=NRctz)vO`2Lc#*z^qnN{Z(?3mbaD^+~v+4PuWv}KIb7sm6U@DQ zm4c3@gI_P>LSc_(IcK+jy=!*kV()CdgSQ<5=U6{=HA}jnt-D`nk3vX|OHjm-g&OBq zKK0YF7doxnA#*tEXMsCQ&+2sbQ(qVDxmuDE9ipQ4eO}I7kS9icXR za;i_nOv!T|Ejq#pEXjVIj8Rd~i%NgBxo!03t=N~>E_I|+Y zmxSaku@U@uUx>U%&L#zrXXYq!J%71+zMj3g%7UzQ<$5~3 zOftXg`c23Ze-_0)hy5BHzZcMDs+AVbwb#crUt|bv_ zx;q?*rMBST6=K zU7Xh6bGc6EaM;ateWjU;yQ0oX{&8u^TYvh@#;K0{tBypNdK}rFd$h*(OSAo#aBgwd zx?k6r`}io%(+9OhiOz;^khiSSF7mCH^ay|!|nea<7+uX3e~ zA*|XfoJ=fYF4S8sFH7CzyZ74SsHWidUBxW+f_-xKUiRh1=HOXzKDvDmXYd_}{%szjJeS{!4`AbeelGi2gemQo+!8 zcmDnly*xAzO1$XZ?pc)KCQZK)oP=IEXhW0uYyk;e44lOnM9L$ zmO_KoQVEd~>sYa8;t>%M2U0DwuU}a?L&9lhpwfKJhdaERuk)^Ia%np>_wIkzh1XwS zJpU_|rPHA&fxGz-!x#5v<-`eVwu@H?UAAxvu(Ar9aIP{9{BZE`9m`MqTlc+R{L!HO$kKeFmV}vx6Xk-- z4<1v`jdD{f>Q3Lfw>>yEp>S1KkVoJ>F4RO-}cosHvGU^{+wU|O z9Gjw}*5t`Ei9?9n@~05vB%am`#u6Xh& z!I36UmS91X1BdSOobz+wQ1D}4HAjS{yV3PSx5H%7@7f2}@0q@A!Dg%86RT^E+uQtX zv^u{3PVGmD0D+?dD^x;P2MO{r^D2giu3se=y1dJkbJaoTf>kOP{>%;OOtfH3c_@9; zh*2eBneVgP?-~}lWC=L$he;B>qyw>hSLjt?dlKiGG9A8vC+%CvmU9jr#iUnOOR;?18y6RF@{i#FoQ&evJ z@H+iKy>dd~=Gdk?BFn=zU0K*I=4TPhx?ICaC*-ok?u3Anpq@M4$BNHJ2k){!9dE$+ zk&T;`vvKQGw~(*;msi+Fxwsy6cU7KIbLD1@(TpUk2z${Uzg3A&M|wJ3e3KW=Qe5ex z6!t-}LuJA~C8g>E!Z&l-ijOp1T+XyG+xbe5lC#|J%S-FDeyoUz6=aF$4!OQ8#W+uP z8ox{MGsm|do4%i$mG5KN+3~*RYvWQF(=Ye^!sn4%A4N2lP0zLtnW6kz7*!N|Iw-S5u8=6 zU&EX9Lj%I1*H*E(>5gt=`nlWM5Z2^_pntGkN(OF@1BB*)K!B zzOs0^zPQs#Noixpk;7K&=S-dQKsU|9X{NAGmU3o>(qBKtg9{>LxQ_l^sG@Ky^TfaT z+GpHmik(0EH7aYfTVwy19?1hYu09ZdFWT{XTG&#j7um`carzyzZrIpWh1OoE$v$(a z|GfW^BWq1_avmL=_()XfiEMAxuFRBlQ`wlPCFfdP6a*#veA?Ek98`*6U7c3At0nX3 zO2gdbs%EDRB7d#t_HT>4qu<8zV>#2sQZ}91(mgxHjy_I{d7-WyVxZ!rBays8`#|)> zhYOh%1w5n$m9KJHU8wr-@GINSJ4>vjSK1ow+#0y0&OGs*`|{_{w!Y80bZ4g9qD`h2 zwbMgC&%5T4dilrIt6pU@S3kKH&JrZ(IboAdv{CnlH7%QTHf!BDAz_@BwXA-RLFI?8 zsc|JMIVWkoi;JBX?qV+Ku_9x(eg3hR^4s;iEp2u=2(o&#KbXEGe}lG8etSzqvVmJi zPomD+OEOEoq;pxlog+}vcHn;ZbG;Bb7TZG&KDI(G!Si26oH#71c>Vf{$GdLKczN+; zR@`14xsvP8{_Ycrm3J~{dCPWn9sey}sVcjS`|TILulg&nMc#yG&npF%vwPZdR92;| zz3Ej~Qm@YM_Po>5OX11?yVG|}SMC$wYhw)EceVKEZTIKelh1Z?sV}~FX-jxY>zC;V zPHTPW_Ij~HCd**kRZq0E^Xg(qh%g7|!6h>$G$5Kdkk7~N}C zT=|5g32?KU?K~8>Q2yPip@@YI#`c z$JCURdxe+2W%w=Y@ei5#;@XMlwWis-bt~`Zb5=G4*r|8%Oign!O4=*WeaWBulIv&t!D%eY7@6!r$HawUfZt4p)~RL&vKuA$wLi1#r3PmlEq9+5i2p`>&;lu?j% zTG+SJ3K^!zxTD5yCAnKY?HEIN zNxUYmj41Hk&0y8&HR-P!Jbo4l*JZsI8)cn(S5f?lz7S*nG*)bzi&^TtQgq&X7 zoogBg_48)v=9&42WL??K`TN_siR|}}e&U#CrupIXXTcA$3G5qYCZ933KXHdw*E4(Z z`)3-PC40}0pR&)swdIm**op(|)Di^$1jXxFbOjm)oqM{(#%1{}FRfkowtabU zncHUff0nqznHTiQ zrS}`>DE+?d8BtX$!F=V#rJ9m?&5CpJ$W*lFku-=%bA31MTOo z<%}#0Hhg>73Um^Fto*EKfAeK*nI)ymx9H)~GCy5stCRvBr2+jeu+_devVn5WM6?@`l)SOv!JcN@5O%#thc zcPKyqon7D7?Dp(+KThQ2^jui8JmG-nftwpx@(jh+s5}Y@4_NKpUKt)$D_G%mBqijr zs!`V&iOMI9hd;eAHZEcNd70zC=)=<*>OZ7DJYZeCi-SpQy?bN$qegXuKgnShs2|krky`C)kr*PtBGpr+{}%d@~XBH zn}0V>w)ybEk#By+`Ckj}yZzvo_$Q|NI9t5;8LQl<|I7|61S*(M$wy}A$*m7xZoXDB z^=_Nk)vbcfg2!ue57byKkbeGUa+6M>i01+i8$I3@kBtWdPAX?r`e^%0N?(!44)^SL zDf#x)=KmX+_CB2pFVjDBl`Ge*jBWrhFo6DuA>HSUosSh;*fg;U79o1Mkpw>I9eY_?xj!k;?F;?Cn28}&ZV zZPcD)-}524C7;=z@zi1V`!$J&>p#{#`&gDB^J0=xj^YQkDbfvdH*VGUe7qviS1Ee+ zu_aSGG|XHkIx;?Ia(yTHMRu>FV|_r&ot=H*a~b8Nnf_}Ynq2bo62re&M2GWuQ8eV{r(~v8LNyrb8XATeSXvnySs5EZG-!3lW-%}@ zq=TH~lbM{FSDea_QBqQ1rLPaRE3+&$IX^cyKd)FXzbIWl2co1{KdDl;I8onN&p=-x zzes@?9X{>>KKe#_hWgR{HpwtILYzk+6hu6>-eh24kOa9gI6tkVJh3R1p}f3YFEcN@ zI61K(738$p?`)(P7#M^hib{);QyKg#D$`T*6g-pj^NQa{tb4)0z@Uzhjt>p;fq4~V fSUy;_ZYD@I$Uyz~=BrMF=CV9p{an^LB{Ts5FplN% literal 0 HcmV?d00001 diff --git a/images/splash.png b/images/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..cc66e5f64cfa889cd4b30df28ae21fe6a42f9c68 GIT binary patch literal 26172 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 literal 0 HcmV?d00001 diff --git a/main.cpp b/main.cpp new file mode 100755 index 0000000..860f310 --- /dev/null +++ b/main.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include +#include + +#include "zeiterfassung.h" +#include "eventloopwithstatus.h" +#include "dialogs/authenticationdialog.h" +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QCoreApplication::setOrganizationDomain(QStringLiteral("brunner.ninja")); + QCoreApplication::setOrganizationName(QStringLiteral("db-software")); + QCoreApplication::setApplicationName(QStringLiteral("zeiterfassung")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + + QSplashScreen splashScreen(QPixmap(":/zeiterfassung/images/splash.png")); + splashScreen.showMessage(QObject::tr("Loading settings...")); + splashScreen.show(); + + QSettings settings; + + if(settings.value("url").isNull()) + { + bool ok; + auto url = QInputDialog::getText(&splashScreen, QObject::tr("Base url"), + QObject::tr("Please enter the base url to the Zeiterfassung:"), + QLineEdit::Normal, QString(), &ok); + if(!ok) + return -1; + settings.setValue("url", url); + } + + splashScreen.showMessage(QObject::tr("Loading login page...")); + + Zeiterfassung erfassung(settings.value("url").toString()); + + { + EventLoopWithStatus eventLoop; + QObject::connect(&erfassung, &Zeiterfassung::loginPageFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + again1: + erfassung.doLoginPage(); + eventLoop.exec(); + + if(!eventLoop.success()) + { + bool ok; + QMessageBox::warning(&splashScreen, QObject::tr("Could not access Zeiterfassung"), + QObject::tr("The Zeiterfassung could not be accessed:\n\n%0").arg(eventLoop.message())); + + auto url = QInputDialog::getText(&splashScreen, QObject::tr("Base url"), + QObject::tr("Please enter the base url to the Zeiterfassung:"), + QLineEdit::Normal, settings.value("url").toString(), &ok); + if(!ok) + return -1; + settings.setValue("url", url); + erfassung.setUrl(url); + goto again1; + } + } + + splashScreen.showMessage(QObject::tr("Authenticating...")); + + if(settings.value("username").isNull() || settings.value("password").isNull()) + { + AuthenticationDialog dialog(&splashScreen); + if(dialog.exec() != QDialog::Accepted) + return -1; + settings.setValue("username", dialog.username()); + settings.setValue("password", dialog.password()); + } + + { + EventLoopWithStatus eventLoop; + QObject::connect(&erfassung, &Zeiterfassung::loginFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + again2: + erfassung.doLogin(settings.value("username").toString(), settings.value("password").toString()); + eventLoop.exec(); + + if(!eventLoop.success()) + { + QMessageBox::warning(&splashScreen, QObject::tr("Could not authenticate with Zeiterfassung"), + QObject::tr("The Zeiterfassung authentication was not successful:\n\n%0").arg(eventLoop.message())); + + AuthenticationDialog dialog(&splashScreen); + dialog.setUsername(settings.value("username").toString()); + dialog.setPassword(settings.value("password").toString()); + if(dialog.exec() != QDialog::Accepted) + return -1; + settings.setValue("username", dialog.username()); + settings.setValue("password", dialog.password()); + + goto again2; + } + } + + splashScreen.showMessage(QObject::tr("Getting user information...")); + + Zeiterfassung::UserInfo userInfo; + + { + EventLoopWithStatus eventLoop; + QObject::connect(&erfassung, &Zeiterfassung::userInfoFinished, + [&](bool success, const QString &message, const Zeiterfassung::UserInfo &_userInfo) { + Q_UNUSED(message) + if(success) + userInfo = _userInfo; + }); + QObject::connect(&erfassung, &Zeiterfassung::userInfoFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + erfassung.doUserInfo(); + eventLoop.exec(); + + if(!eventLoop.success()) + { + QMessageBox::warning(&splashScreen, QObject::tr("Could not get user information!"), + QObject::tr("Could not get user information:\n\n%0").arg(eventLoop.message())); + + return -1; + } + } + + MainWindow mainWindow(settings, erfassung, userInfo); + mainWindow.show(); + + splashScreen.finish(&mainWindow); + + return app.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..aa47820 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,978 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "eventloopwithstatus.h" +#include "dialogs/aboutmedialog.h" +#include "dialogs/buchungdialog.h" +#include "dialogs/kontierungdialog.h" +#include "models/buchungenmodel.h" +#include "models/kontierungenmodel.h" + +MainWindow::MainWindow(QSettings &settings, Zeiterfassung &erfassung, const Zeiterfassung::UserInfo &userInfo, QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow), + m_settings(settings), + m_erfassung(erfassung), + m_userInfo(userInfo), + m_buchungenModel(new BuchungenModel(erfassung, this)), + m_kontierungenModel(new KontierungenModel(erfassung, this)), + m_flag(false) +{ + ui->setupUi(this); + + setWindowTitle(tr("Zeiterfassung - %0").arg(m_userInfo.text)); + + ui->actionQuit->setShortcut(QKeySequence::Quit); + + connect(ui->actionToday, &QAction::triggered, [=](){ ui->dateEditDate->setDate(QDate::currentDate()); }); + + ui->actionRefresh->setShortcut(QKeySequence::Refresh); + connect(ui->actionRefresh, &QAction::triggered, this, &MainWindow::refresh); + + connect(ui->actionAboutMe, &QAction::triggered, [=](){ AboutMeDialog(userInfo, this).exec(); }); + connect(ui->actionAboutQt, &QAction::triggered, [=](){ QMessageBox::aboutQt(this); }); + + ui->dateEditDate->setDate(QDate::currentDate()); + connect(ui->dateEditDate, &QDateTimeEdit::dateChanged, this, &MainWindow::refresh); + refresh(); + + connect(ui->pushButtonPrev, &QAbstractButton::pressed, this, &MainWindow::pushButtonPrevPressed); + connect(ui->pushButtonNext, &QAbstractButton::pressed, this, &MainWindow::pushButtonNextPressed); + + ui->timeEditTime->setTime(timeNormalise(QTime::currentTime())); + + connect(&m_erfassung, &Zeiterfassung::getProjekteFinished, + this, &MainWindow::getProjekteFinished); + erfassung.doGetProjekte(userInfo.userId); + + ui->comboBoxProjekt->setMaxVisibleItems(10); + + ui->comboBoxSubprojekt->lineEdit()->setPlaceholderText(tr("Subprojekt")); + ui->comboBoxWorkpackage->lineEdit()->setPlaceholderText(tr("Workpackage")); + ui->comboBoxText->lineEdit()->setPlaceholderText(tr("Text")); + + updateComboboxes(); + + connect(ui->pushButtonStart, &QAbstractButton::pressed, this, &MainWindow::pushButtonStartPressed); + connect(ui->pushButtonEnd, &QAbstractButton::pressed, this, &MainWindow::pushButtonEndPressed); + + ui->treeViewBuchungen->setModel(m_buchungenModel); + ui->treeViewKontierungen->setModel(m_kontierungenModel); + + connect(ui->treeViewBuchungen, &QWidget::customContextMenuRequested, + this, &MainWindow::contextMenuBuchung); + connect(ui->treeViewKontierungen, &QWidget::customContextMenuRequested, + this, &MainWindow::contextMenuKontierung); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::refresh() +{ + ui->actionToday->setEnabled(false); + ui->actionRefresh->setEnabled(false); + ui->dateEditDate->setReadOnly(true); + ui->timeEditTime->setEnabled(false); + ui->comboBoxProjekt->setEnabled(false); + ui->comboBoxSubprojekt->setEnabled(false); + ui->comboBoxWorkpackage->setEnabled(false); + ui->comboBoxText->setEnabled(false); + ui->pushButtonStart->setEnabled(false); + ui->pushButtonEnd->setEnabled(false); + ui->treeViewBuchungen->setEnabled(false); + ui->treeViewKontierungen->setEnabled(false); + + auto waitForBuchugen = m_buchungenModel->refresh(m_userInfo.userId, ui->dateEditDate->date(), ui->dateEditDate->date()); + if(waitForBuchugen) + { + connect(m_buchungenModel, &BuchungenModel::refreshFinished, + this, &MainWindow::refreshBuchungenFinished); + } + + auto waitForKontierungen = m_kontierungenModel->refresh(m_userInfo.userId, ui->dateEditDate->date(), ui->dateEditDate->date()); + if(waitForKontierungen) + { + connect(m_kontierungenModel, &KontierungenModel::refreshFinished, + this, &MainWindow::refreshKontierungenFinished); + } + + if(!waitForBuchugen || !waitForKontierungen) + QMessageBox::warning(this, tr("Unknown error occured."), tr("An unknown error occured.")); + + if(waitForBuchugen || waitForKontierungen) + m_flag = waitForBuchugen == waitForKontierungen; + else + { + ui->actionToday->setEnabled(true); + ui->actionRefresh->setEnabled(true); + ui->dateEditDate->setReadOnly(false); + } +} + +void MainWindow::getProjekteFinished(bool success, const QString &message, const QVector &projekte) +{ + disconnect(&m_erfassung, &Zeiterfassung::getProjekteFinished, + this, &MainWindow::getProjekteFinished); + + if(!success) + { + QMessageBox::warning(this, tr("Could not load Buchungen!"), tr("Could not load Buchungen:\n\n%0").arg(message)); + return; + } + + m_projekte = projekte; + + updateComboboxes(); +} + +void MainWindow::refreshBuchungenFinished(bool success, const QString &message) +{ + disconnect(m_buchungenModel, &BuchungenModel::refreshFinished, + this, &MainWindow::refreshBuchungenFinished); + + if(success) + ui->treeViewBuchungen->setEnabled(true); + + if(m_flag) + m_flag = false; + else + validateEntries(); + + if(!success) + QMessageBox::warning(Q_NULLPTR, tr("Could not refresh Buchungen!"), tr("Could not refresh Buchungen:\n\n%0").arg(message)); +} + +void MainWindow::refreshKontierungenFinished(bool success, const QString &message) +{ + disconnect(m_kontierungenModel, &KontierungenModel::refreshFinished, + this, &MainWindow::refreshKontierungenFinished); + + if(success) + ui->treeViewKontierungen->setEnabled(true); + + if(m_flag) + m_flag = false; + else + validateEntries(); + + if(!success) + QMessageBox::warning(Q_NULLPTR, tr("Could not refresh Kontierungen!"), tr("Could not refresh Kontierungen:\n\n%0").arg(message)); +} + +void MainWindow::contextMenuBuchung(const QPoint &pos) +{ + auto index = ui->treeViewBuchungen->indexAt(pos); + + if(index.isValid()) + { + auto buchung = m_buchungenModel->getBuchung(index); + + QMenu menu; + auto editAction = menu.addAction(tr("Edit")); + auto deleteAction = menu.addAction(tr("Delete")); + auto selectedAction = menu.exec(ui->treeViewBuchungen->viewport()->mapToGlobal(pos)); + if(selectedAction == editAction) + { + BuchungDialog dialog(this); + dialog.setTime(buchung.time); + dialog.setTimespan(buchung.timespan); + dialog.setType(buchung.type); + dialog.setText(buchung.text); + again1: + if(dialog.exec() == QDialog::Accepted) + { + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::updateBuchungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + m_erfassung.doUpdateBuchung(buchung.id, m_userInfo.userId, ui->dateEditDate->date(), + dialog.getTime(), dialog.getTimespan(), + dialog.getType(), dialog.getText()); + eventLoop.exec(); + + if(eventLoop.success()) + { + ui->actionToday->setEnabled(false); + ui->actionRefresh->setEnabled(false); + ui->dateEditDate->setReadOnly(true); + ui->timeEditTime->setEnabled(false); + ui->comboBoxProjekt->setEnabled(false); + ui->comboBoxSubprojekt->setEnabled(false); + ui->comboBoxWorkpackage->setEnabled(false); + ui->comboBoxText->setEnabled(false); + ui->pushButtonStart->setEnabled(false); + ui->pushButtonEnd->setEnabled(false); + ui->treeViewBuchungen->setEnabled(false); + + if(m_buchungenModel->refresh(m_userInfo.userId, ui->dateEditDate->date(), ui->dateEditDate->date())) + { + connect(m_buchungenModel, &BuchungenModel::refreshFinished, + this, &MainWindow::refreshBuchungenFinished); + m_flag = false; + } + else + { + ui->actionToday->setEnabled(true); + ui->actionRefresh->setEnabled(true); + ui->dateEditDate->setReadOnly(false); + } + } + else + { + QMessageBox::warning(this, tr("Could not update Buchung!"), tr("Could not update Buchung:\n\n%0").arg(eventLoop.message())); + goto again1; + } + } + } + else if(selectedAction == deleteAction) + { + QMessageBox msgBox; + msgBox.setText("Do you really want to delete the Buchung?"); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + if(msgBox.exec() == QMessageBox::Yes) + { + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::deleteBuchungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + m_erfassung.doDeleteBuchung(buchung.id); + eventLoop.exec(); + + if(eventLoop.success()) + { + ui->actionToday->setEnabled(false); + ui->actionRefresh->setEnabled(false); + ui->dateEditDate->setReadOnly(true); + ui->timeEditTime->setEnabled(false); + ui->comboBoxProjekt->setEnabled(false); + ui->comboBoxSubprojekt->setEnabled(false); + ui->comboBoxWorkpackage->setEnabled(false); + ui->comboBoxText->setEnabled(false); + ui->pushButtonStart->setEnabled(false); + ui->pushButtonEnd->setEnabled(false); + ui->treeViewBuchungen->setEnabled(false); + + if(m_buchungenModel->refresh(m_userInfo.userId, ui->dateEditDate->date(), ui->dateEditDate->date())) + { + connect(m_buchungenModel, &BuchungenModel::refreshFinished, + this, &MainWindow::refreshBuchungenFinished); + m_flag = false; + } + else + { + ui->actionToday->setEnabled(true); + ui->actionRefresh->setEnabled(true); + ui->dateEditDate->setReadOnly(false); + } + } + else + QMessageBox::warning(this, tr("Could not delete Buchung!"), tr("Could not delete Buchung:\n\n%0").arg(eventLoop.message())); + } + } + } + else + { + QMenu menu; + auto createAction = menu.addAction(tr("Create")); + auto selectedAction = menu.exec(ui->treeViewBuchungen->viewport()->mapToGlobal(pos)); + if(selectedAction == createAction) + { + BuchungDialog dialog(this); + dialog.setTime(QTime::currentTime()); + again2: + if(dialog.exec() == QDialog::Accepted) + { + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::createBuchungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + m_erfassung.doCreateBuchung(m_userInfo.userId, ui->dateEditDate->date(), + dialog.getTime(), dialog.getTimespan(), + dialog.getType(), dialog.getText()); + eventLoop.exec(); + + if(eventLoop.success()) + { + ui->actionToday->setEnabled(false); + ui->actionRefresh->setEnabled(false); + ui->dateEditDate->setReadOnly(true); + ui->timeEditTime->setEnabled(false); + ui->comboBoxProjekt->setEnabled(false); + ui->comboBoxSubprojekt->setEnabled(false); + ui->comboBoxWorkpackage->setEnabled(false); + ui->comboBoxText->setEnabled(false); + ui->pushButtonStart->setEnabled(false); + ui->pushButtonEnd->setEnabled(false); + ui->treeViewBuchungen->setEnabled(false); + + if(m_buchungenModel->refresh(m_userInfo.userId, ui->dateEditDate->date(), ui->dateEditDate->date())) + { + connect(m_buchungenModel, &BuchungenModel::refreshFinished, + this, &MainWindow::refreshBuchungenFinished); + m_flag = false; + } + else + { + ui->actionToday->setEnabled(true); + ui->actionRefresh->setEnabled(true); + ui->dateEditDate->setReadOnly(false); + } + } + else + { + QMessageBox::warning(this, tr("Could not create Buchung!"), tr("Could not create Buchung:\n\n%0").arg(eventLoop.message())); + goto again2; + } + } + } + } +} + +void MainWindow::contextMenuKontierung(const QPoint &pos) +{ + auto index = ui->treeViewKontierungen->indexAt(pos); + + if(index.isValid()) + { + auto kontierung = m_kontierungenModel->getKontierung(index); + + QMenu menu; + auto editAction = menu.addAction(tr("Edit")); + auto deleteAction = menu.addAction(tr("Delete")); + auto selectedAction = menu.exec(ui->treeViewKontierungen->viewport()->mapToGlobal(pos)); + if(selectedAction == editAction) + { + KontierungDialog dialog(m_erfassung, m_userInfo, m_projekte, this); + dialog.setTime(kontierung.time); + dialog.setTimespan(kontierung.timespan); + dialog.setProjekt(kontierung.projekt); + dialog.setSubprojekt(kontierung.subprojekt); + dialog.setWorkpackage(kontierung.workpackage); + dialog.setText(kontierung.text); + again1: + if(dialog.exec() == QDialog::Accepted) + { + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::updateKontierungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + m_erfassung.doUpdateKontierung(kontierung.id, m_userInfo.userId, ui->dateEditDate->date(), + dialog.getTime(), dialog.getTimespan(), + dialog.getProjekt(), dialog.getSubprojekt(), + dialog.getWorkpackage(), dialog.getText()); + eventLoop.exec(); + + if(eventLoop.success()) + { + ui->actionToday->setEnabled(false); + ui->actionRefresh->setEnabled(false); + ui->dateEditDate->setReadOnly(true); + ui->timeEditTime->setEnabled(false); + ui->comboBoxProjekt->setEnabled(false); + ui->comboBoxSubprojekt->setEnabled(false); + ui->comboBoxWorkpackage->setEnabled(false); + ui->comboBoxText->setEnabled(false); + ui->pushButtonStart->setEnabled(false); + ui->pushButtonEnd->setEnabled(false); + ui->treeViewKontierungen->setEnabled(false); + + if(m_kontierungenModel->refresh(m_userInfo.userId, ui->dateEditDate->date(), ui->dateEditDate->date())) + { + connect(m_kontierungenModel, &KontierungenModel::refreshFinished, + this, &MainWindow::refreshKontierungenFinished); + m_flag = false; + } + else + { + ui->actionToday->setEnabled(true); + ui->actionRefresh->setEnabled(true); + ui->dateEditDate->setReadOnly(false); + } + } + else + { + QMessageBox::warning(this, tr("Could not update Kontierung!"), tr("Could not update Kontierung:\n\n%0").arg(eventLoop.message())); + goto again1; + } + } + } + else if(selectedAction == deleteAction) + { + QMessageBox msgBox; + msgBox.setText("Do you really want to delete the Kontierung?"); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + if(msgBox.exec() == QMessageBox::Yes) + { + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::deleteKontierungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + m_erfassung.doDeleteKontierung(kontierung.id); + eventLoop.exec(); + + if(eventLoop.success()) + { + ui->actionToday->setEnabled(false); + ui->actionRefresh->setEnabled(false); + ui->dateEditDate->setReadOnly(true); + ui->timeEditTime->setEnabled(false); + ui->comboBoxProjekt->setEnabled(false); + ui->comboBoxSubprojekt->setEnabled(false); + ui->comboBoxWorkpackage->setEnabled(false); + ui->comboBoxText->setEnabled(false); + ui->pushButtonStart->setEnabled(false); + ui->pushButtonEnd->setEnabled(false); + ui->treeViewKontierungen->setEnabled(false); + + if(m_kontierungenModel->refresh(m_userInfo.userId, ui->dateEditDate->date(), ui->dateEditDate->date())) + { + connect(m_kontierungenModel, &KontierungenModel::refreshFinished, + this, &MainWindow::refreshKontierungenFinished); + m_flag = false; + } + else + { + ui->actionToday->setEnabled(true); + ui->actionRefresh->setEnabled(true); + ui->dateEditDate->setReadOnly(false); + } + } + else + QMessageBox::warning(this, tr("Could not delete Kontierung!"), tr("Could not delete Kontierung:\n\n%0").arg(eventLoop.message())); + } + } + } + else + { + QMenu menu; + auto createAction = menu.addAction(tr("Create")); + auto selectedAction = menu.exec(ui->treeViewKontierungen->viewport()->mapToGlobal(pos)); + if(selectedAction == createAction) + { + KontierungDialog dialog(m_erfassung, m_userInfo, m_projekte, this); + again2: + if(dialog.exec() == QDialog::Accepted) + { + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::createKontierungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + m_erfassung.doCreateKontierung(m_userInfo.userId, ui->dateEditDate->date(), + dialog.getTime(), dialog.getTimespan(), + dialog.getProjekt(), dialog.getSubprojekt(), + dialog.getWorkpackage(), dialog.getText()); + eventLoop.exec(); + + if(eventLoop.success()) + { + ui->actionToday->setEnabled(false); + ui->actionRefresh->setEnabled(false); + ui->dateEditDate->setReadOnly(true); + ui->timeEditTime->setEnabled(false); + ui->comboBoxProjekt->setEnabled(false); + ui->comboBoxSubprojekt->setEnabled(false); + ui->comboBoxWorkpackage->setEnabled(false); + ui->comboBoxText->setEnabled(false); + ui->pushButtonStart->setEnabled(false); + ui->pushButtonEnd->setEnabled(false); + ui->treeViewKontierungen->setEnabled(false); + + if(m_kontierungenModel->refresh(m_userInfo.userId, ui->dateEditDate->date(), ui->dateEditDate->date())) + { + connect(m_kontierungenModel, &KontierungenModel::refreshFinished, + this, &MainWindow::refreshKontierungenFinished); + m_flag = false; + } + else + { + ui->actionToday->setEnabled(true); + ui->actionRefresh->setEnabled(true); + ui->dateEditDate->setReadOnly(false); + } + } + else + { + QMessageBox::warning(this, tr("Could not create Kontierung!"), tr("Could not create Kontierung:\n\n%0").arg(eventLoop.message())); + goto again2; + } + } + } + } +} + +void MainWindow::pushButtonPrevPressed() +{ + ui->dateEditDate->setDate(ui->dateEditDate->date().addDays(-1)); +} + +void MainWindow::pushButtonNextPressed() +{ + ui->dateEditDate->setDate(ui->dateEditDate->date().addDays(1)); +} + +void MainWindow::pushButtonStartPressed() +{ + if(m_buchungenModel->rbegin() == m_buchungenModel->rend() || + m_buchungenModel->rbegin()->type == QStringLiteral("G")) + { + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::createBuchungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + m_erfassung.doCreateBuchung(m_userInfo.userId, ui->dateEditDate->date(), + timeNormalise(ui->timeEditTime->time()), QTime(0, 0), + QStringLiteral("K"), QStringLiteral("")); + eventLoop.exec(); + + if(!eventLoop.success()) + { + QMessageBox::warning(this, tr("Could not create Buchung!"), tr("Could not create Buchung:\n\n%0").arg(eventLoop.message())); + refresh(); + return; + } + } + + if(m_kontierungenModel->rbegin() != m_kontierungenModel->rend()) + { + auto kontierung = *m_kontierungenModel->rbegin(); + if(kontierung.timespan == QTime(0, 0)) + { + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::updateKontierungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + auto timespan = timeBetween(m_lastKontierungStart, ui->timeEditTime->time()); + qDebug() << "timespan" << timespan; + + m_erfassung.doUpdateKontierung(kontierung.id, m_userInfo.userId, kontierung.date, + kontierung.time, timespan, + kontierung.projekt, kontierung.subprojekt, + kontierung.workpackage, kontierung.text); + eventLoop.exec(); + + if(eventLoop.success()) + m_kontierungTime = timeAdd(m_kontierungTime, timespan); + else + { + QMessageBox::warning(this, tr("Could not update Kontierung!"), tr("Could not update Kontierung:\n\n%0").arg(eventLoop.message())); + refresh(); + return; + } + } + } + + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::createKontierungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + m_erfassung.doCreateKontierung(m_userInfo.userId, ui->dateEditDate->date(), + m_kontierungTime, QTime(0, 0), + ui->comboBoxProjekt->currentData().toString(), ui->comboBoxSubprojekt->currentText(), + ui->comboBoxWorkpackage->currentText(), ui->comboBoxText->currentText()); + eventLoop.exec(); + + if(!eventLoop.success()) + { + QMessageBox::warning(this, tr("Could not create Kontierung!"), tr("Could not create Kontierung:\n\n%0").arg(eventLoop.message())); + refresh(); + return; + } + + { + QStringList projekte = m_settings.value("projekte", QStringList()).toStringList(); + projekte.removeAll(ui->comboBoxProjekt->currentData().toString()); + projekte.prepend(ui->comboBoxProjekt->currentData().toString()); + m_settings.setValue("projekte", projekte); + } + + if(!ui->comboBoxSubprojekt->currentText().trimmed().isEmpty()) + { + QStringList subprojekte = m_settings.value("subprojekte", QStringList()).toStringList(); + subprojekte.removeAll(ui->comboBoxSubprojekt->currentText()); + subprojekte.prepend(ui->comboBoxSubprojekt->currentText()); + m_settings.setValue("subprojekte", subprojekte); + } + + if(!ui->comboBoxWorkpackage->currentText().trimmed().isEmpty()) + { + QStringList workpackages = m_settings.value("workpackages", QStringList()).toStringList(); + workpackages.removeAll(ui->comboBoxWorkpackage->currentText()); + workpackages.prepend(ui->comboBoxWorkpackage->currentText()); + m_settings.setValue("workpackages", workpackages); + } + + if(!ui->comboBoxText->currentText().trimmed().isEmpty()) + { + QStringList texte = m_settings.value("texte", QStringList()).toStringList(); + texte.removeAll(ui->comboBoxText->currentText()); + texte.prepend(ui->comboBoxText->currentText()); + m_settings.setValue("texte", texte); + } + + updateComboboxes(); + + refresh(); +} + +void MainWindow::pushButtonEndPressed() +{ + { + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::createBuchungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + m_erfassung.doCreateBuchung(m_userInfo.userId, ui->dateEditDate->date(), + timeNormalise(ui->timeEditTime->time()), QTime(0, 0), + QStringLiteral("G"), QStringLiteral("")); + eventLoop.exec(); + + if(!eventLoop.success()) + { + QMessageBox::warning(this, tr("Could not create Buchung!"), tr("Could not create Buchung:\n\n%0").arg(eventLoop.message())); + refresh(); + return; + } + } + + { + auto kontierung = *m_kontierungenModel->rbegin(); + Q_ASSERT(kontierung.timespan == QTime(0, 0)); + + EventLoopWithStatus eventLoop; + connect(&m_erfassung, &Zeiterfassung::updateKontierungFinished, &eventLoop, &EventLoopWithStatus::quitWithStatus); + + auto timespan = timeBetween(m_lastKontierungStart, ui->timeEditTime->time()); + qDebug() << "timespan" << timespan; + + m_erfassung.doUpdateKontierung(kontierung.id, m_userInfo.userId, kontierung.date, + kontierung.time, timespan, + kontierung.projekt, kontierung.subprojekt, + kontierung.workpackage, kontierung.text); + eventLoop.exec(); + + if(eventLoop.success()) + m_kontierungTime = timeAdd(m_kontierungTime, timespan); + else + { + QMessageBox::warning(this, tr("Could not update Kontierung!"), tr("Could not update Kontierung:\n\n%0").arg(eventLoop.message())); + refresh(); + return; + } + } + + refresh(); +} + +void MainWindow::validateEntries() +{ + ui->actionToday->setEnabled(true); + ui->actionRefresh->setEnabled(true); + ui->dateEditDate->setReadOnly(false); + ui->pushButtonStart->setText(tr("Start")); + + if(!ui->treeViewBuchungen->isEnabled()) + return; + + if(!ui->treeViewKontierungen->isEnabled()) + return; + + auto buchungenIter = m_buchungenModel->constBegin(); + auto kontierungenIter = m_kontierungenModel->constBegin(); + + m_kontierungTime = QTime(0, 0); + auto buchungTimespan = QTime(0, 0); + + while(true) + { + if(buchungenIter == m_buchungenModel->constEnd() && + kontierungenIter == m_kontierungenModel->constEnd()) + { + ui->timeEditTime->setEnabled(true); + ui->comboBoxProjekt->setEnabled(true); + ui->comboBoxSubprojekt->setEnabled(true); + ui->comboBoxWorkpackage->setEnabled(true); + ui->comboBoxText->setEnabled(true); + ui->pushButtonStart->setEnabled(true); + + return; + } + + if(buchungenIter == m_buchungenModel->constEnd()) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("Missing Buchung."))); + return; + } + + auto startBuchung = *buchungenIter++; + qDebug() << "startBuchung" << startBuchung.time; + if(startBuchung.type != QStringLiteral("K")) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("Expected Buchung for Kommen, instead got type %0\nBuchung ID: %1").arg(startBuchung.type).arg(startBuchung.id))); + return; + } + + m_lastKontierungStart = startBuchung.time; + + if(kontierungenIter == m_kontierungenModel->constEnd()) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("Missing Kontierung."))); + return; + } + + auto kontierung = *kontierungenIter++; + if(kontierung.time != m_kontierungTime) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("Expected time %0 but got %1 Kontierung.\nKontierung ID: %2") + .arg(m_kontierungTime.toString("HH:mm:ss")) + .arg(kontierung.time.toString("HH:mm:ss")) + .arg(kontierung.id))); + return; + } + + if(kontierung.timespan == QTime(0, 0)) + { + if(buchungenIter != m_buchungenModel->constEnd()) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("There is another Buchung after an unfinished Kontierung.\nBuchung ID: %0\nKontierung ID: %1") + .arg(buchungenIter->id) + .arg(kontierung.id))); + return; + } + + if(kontierungenIter != m_kontierungenModel->constEnd()) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("There is another Kontierung after an unfinished Kontierung.\nKontierung ID: %0\nKontierung ID: %1") + .arg(kontierungenIter->id) + .arg(kontierung.id))); + return; + } + + ui->pushButtonStart->setText(tr("Switch")); + ui->pushButtonEnd->setEnabled(true); + goto after; + } + else + { + m_kontierungTime = timeAdd(m_kontierungTime, kontierung.timespan); + m_lastKontierungStart = timeAdd(m_lastKontierungStart, kontierung.timespan); + + if(buchungenIter == m_buchungenModel->constEnd()) + { + while(true) + { + if(kontierungenIter == m_kontierungenModel->constEnd()) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("The last Kontierung is finished without Gehen-Buchung\nKontierung ID: %0") + .arg(kontierung.id))); + return; + } + + kontierung = *kontierungenIter++; + if(kontierung.time != m_kontierungTime) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("Expected time %0 but got %1 Kontierung.\nKontierung ID: %2") + .arg(m_kontierungTime.toString("HH:mm:ss")) + .arg(kontierung.time.toString("HH:mm:ss")) + .arg(kontierung.id))); + return; + } + + if(kontierung.timespan == QTime(0, 0)) + { + if(kontierungenIter != m_kontierungenModel->constEnd()) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("There is another Kontierung after an unfinished Kontierung.\nKontierung ID: %0\nKontierung ID: %1") + .arg(kontierung.id) + .arg(kontierungenIter->id))); + return; + } + + ui->pushButtonStart->setText(tr("Switch")); + ui->pushButtonEnd->setEnabled(true); + goto after; + } + else + { + m_kontierungTime = timeAdd(m_kontierungTime, kontierung.timespan); + m_lastKontierungStart = timeAdd(m_lastKontierungStart, kontierung.timespan); + } + } + } + else + { + auto endBuchung = *buchungenIter++; + qDebug() << "endBuchung" << endBuchung.time; + if(endBuchung.type != QStringLiteral("G")) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("Expected Buchung for Gehen, instead got type %0\nBuchung ID: %1").arg(endBuchung.type).arg(endBuchung.id))); + return; + } + + buchungTimespan = timeAdd(buchungTimespan, timeBetween(startBuchung.time, endBuchung.time)); + qDebug() << "buchungTimespan" << buchungTimespan; + + while(m_kontierungTime < buchungTimespan) + { + if(kontierungenIter == m_kontierungenModel->constEnd()) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("Missing Kontierung! Time not filled: %0 - %1") + .arg(m_kontierungTime.toString("HH:mm:ss")) + .arg(buchungTimespan.toString("HH:mm:ss")))); + return; + } + + kontierung = *kontierungenIter++; + if(kontierung.time != m_kontierungTime) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("Expected time %0 but got %1 Kontierung.\nKontierung ID: %2") + .arg(m_kontierungTime.toString("HH:mm:ss")) + .arg(kontierung.time.toString("HH:mm:ss")) + .arg(kontierung.id))); + return; + } + + if(kontierung.timespan == QTime(0, 0)) + { + if(buchungenIter != m_buchungenModel->constEnd()) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("There is another Buchung after an unfinished Kontierung.\nBuchung ID: %0\nKontierung ID: %1") + .arg(buchungenIter->id) + .arg(kontierung.id))); + return; + } + + if(kontierungenIter != m_kontierungenModel->constEnd()) + { + QMessageBox::warning(this, tr("Illegal state!"), tr("Your Buchungen and Kontierungen for this day are in an invalid state:\n\n%0") + .arg(tr("There is another Kontierung after an unfinished Kontierung.\nKontierung ID: %0\nKontierung ID: %1") + .arg(kontierungenIter->id) + .arg(kontierung.id))); + return; + } + + ui->pushButtonStart->setText(tr("Switch")); + ui->pushButtonEnd->setEnabled(true); + goto after; + } + else + { + m_kontierungTime = timeAdd(m_kontierungTime, kontierung.timespan); + } + } + } + } + } + + after: + + qDebug() << "m_kontierTime" << m_kontierungTime; + + ui->timeEditTime->setEnabled(true); + ui->comboBoxProjekt->setEnabled(true); + ui->comboBoxSubprojekt->setEnabled(true); + ui->comboBoxWorkpackage->setEnabled(true); + ui->comboBoxText->setEnabled(true); + ui->pushButtonStart->setEnabled(true); +} + +void MainWindow::updateComboboxes() +{ + ui->comboBoxProjekt->clear(); + + { + auto preferedProjekte = m_settings.value("projekte", QStringList()).toStringList(); + + for(const auto &preferedProjekt : preferedProjekte) + { + for(const auto &projekt : m_projekte) + { + if(preferedProjekt == projekt.value) + ui->comboBoxProjekt->addItem(projekt.label % " (" % projekt.value % ')', projekt.value); + } + } + + if(preferedProjekte.count()) + { + ui->comboBoxProjekt->addItem(QStringLiteral("--------------")); + + auto model = qobject_cast(ui->comboBoxProjekt->model()); + auto item = model->item(ui->comboBoxProjekt->count() - 1); + item->setFlags(item->flags() & ~(Qt::ItemIsSelectable|Qt::ItemIsEnabled)); + } + + for(const auto &projekt : m_projekte) + { + if(!preferedProjekte.contains(projekt.value)) + ui->comboBoxProjekt->addItem(projekt.label % " (" % projekt.value % ')', projekt.value); + } + } + + ui->comboBoxSubprojekt->clear(); + + { + auto subprojekte = m_settings.value("subprojekte", QStringList()).toStringList(); + for(const auto &subprojekt : subprojekte) + ui->comboBoxSubprojekt->addItem(subprojekt); + if(subprojekte.count()) + ui->comboBoxSubprojekt->setCurrentText(QString()); + } + + ui->comboBoxWorkpackage->clear(); + + { + auto workpackages = m_settings.value("workpackages", QStringList()).toStringList(); + for(const auto &workpackage : workpackages) + ui->comboBoxWorkpackage->addItem(workpackage); + if(workpackages.count()) + ui->comboBoxWorkpackage->setCurrentText(QString()); + } + + ui->comboBoxText->clear(); + + { + auto texte = m_settings.value("texte", QStringList()).toStringList(); + for(const auto &text : texte) + ui->comboBoxText->addItem(text); + if(texte.count()) + ui->comboBoxText->setCurrentText(QString()); + } +} + +int MainWindow::timeToSeconds(const QTime &time) +{ + return QTime(0, 0).secsTo(time); +} + +QTime MainWindow::timeBetween(const QTime &l, const QTime &r) +{ + Q_ASSERT(l <= r); + return QTime(0, 0).addSecs(l.secsTo(r)); +} + +QTime MainWindow::timeAdd(const QTime &l, const QTime &r) +{ + Q_ASSERT(timeToSeconds(l) + timeToSeconds(r) < 86400); + return l.addSecs(QTime(0, 0).secsTo(r)); +} + +QTime MainWindow::timeNormalise(const QTime &time) +{ + return time.addSecs(-time.second()); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..65eaa87 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,57 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include + +#include "zeiterfassung.h" + +namespace Ui { class MainWindow; } +class BuchungenModel; +class KontierungenModel; + +class QSettings; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QSettings &settings, Zeiterfassung &erfassung, const Zeiterfassung::UserInfo &userInfo, QWidget *parent = 0); + ~MainWindow(); + +private Q_SLOTS: + void refresh(); + void getProjekteFinished(bool success, const QString &message, const QVector &projekte); + void refreshBuchungenFinished(bool success, const QString &message); + void refreshKontierungenFinished(bool success, const QString &message); + void contextMenuBuchung(const QPoint &pos); + void contextMenuKontierung(const QPoint &pos); + void pushButtonPrevPressed(); + void pushButtonNextPressed(); + void pushButtonStartPressed(); + void pushButtonEndPressed(); + +private: + void validateEntries(); + void updateComboboxes(); + + static int timeToSeconds(const QTime &time); + static QTime timeBetween(const QTime &l, const QTime &r); + static QTime timeAdd(const QTime &l, const QTime &r); + static QTime timeNormalise(const QTime &time); + + Ui::MainWindow *ui; + QSettings &m_settings; + Zeiterfassung &m_erfassung; + const Zeiterfassung::UserInfo &m_userInfo; + QVector m_projekte; + + BuchungenModel *m_buchungenModel; + KontierungenModel *m_kontierungenModel; + + bool m_flag; + QTime m_kontierungTime; + QTime m_lastKontierungStart; +}; + +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..01ff48d --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,322 @@ + + + MainWindow + + + + 0 + 0 + 1242 + 413 + + + + MainWindow + + + + + + + + + true + + + + + + + + + + + :/zeiterfassung/images/previous.png:/zeiterfassung/images/previous.png + + + + + + + + + + + :/zeiterfassung/images/next.png:/zeiterfassung/images/next.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + + + + + false + + + + 200 + 0 + + + + + + + + false + + + + 150 + 0 + + + + true + + + QComboBox::NoInsert + + + + + + + false + + + + 150 + 0 + + + + true + + + QComboBox::NoInsert + + + + + + + false + + + + 150 + 0 + + + + true + + + QComboBox::NoInsert + + + + + + + false + + + Start + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + End + + + + + + + + + Qt::Vertical + + + + Buchungen + + + + + + false + + + Qt::CustomContextMenu + + + + + + + + Kontierungen + + + + + + false + + + Qt::CustomContextMenu + + + + + + + + + + + + + 0 + 0 + 1242 + 22 + + + + + &File + + + + + + &About + + + + + + + + &View + + + + + + + + + + + + + :/zeiterfassung/images/quit.png:/zeiterfassung/images/quit.png + + + &Quit + + + + + About &Me + + + + + About &zeiterfassung + + + + + About &Qt + + + + + false + + + + :/zeiterfassung/images/today.png:/zeiterfassung/images/today.png + + + &Today + + + + + false + + + + :/zeiterfassung/images/refresh.png:/zeiterfassung/images/refresh.png + + + &Refresh + + + + + + + + + actionQuit + triggered() + MainWindow + close() + + + -1 + -1 + + + 458 + 296 + + + + + diff --git a/models/buchungenmodel.cpp b/models/buchungenmodel.cpp new file mode 100644 index 0000000..1abf472 --- /dev/null +++ b/models/buchungenmodel.cpp @@ -0,0 +1,106 @@ +#include "buchungenmodel.h" + +BuchungenModel::BuchungenModel(Zeiterfassung &erfassung, QObject *parent) : + QAbstractListModel(parent), + m_erfassung(erfassung) +{ + +} + +int BuchungenModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return m_buchungen.count(); +} + +int BuchungenModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return 5; +} + +QVariant BuchungenModel::data(const QModelIndex &index, int role) const +{ + Q_ASSERT(index.row() < m_buchungen.count()); + const auto &buchung = m_buchungen.at(index.row()); + + switch(role) + { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) + { + case 0: return buchung.id; + case 1: return buchung.time; + case 2: return buchung.timespan; + case 3: return buchung.type; + case 4: return buchung.text; + } + } + + return QVariant(); +} + +QVariant BuchungenModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch(orientation) + { + case Qt::Horizontal: + switch(role) + { + case Qt::DisplayRole: + case Qt::EditRole: + switch(section) + { + case 0: return tr("ID"); + case 1: return tr("Time"); + case 2: return tr("Timespan"); + case 3: return tr("Type"); + case 4: return tr("Text"); + } + } + } + + return QVariant(); +} + +Zeiterfassung::Buchung BuchungenModel::getBuchung(const QModelIndex &index) const +{ + if(!index.isValid()) + return Zeiterfassung::Buchung(); + + Q_ASSERT(index.row() <= m_buchungen.count()); + return m_buchungen.at(index.row()); +} + +bool BuchungenModel::refresh(int userId, const QDate &from, const QDate &to) +{ + if(!m_erfassung.doGetBuchungen(userId, from, to)) + return false; + + beginResetModel(); + m_buchungen.clear(); + endResetModel(); + + connect(&m_erfassung, &Zeiterfassung::getBuchungenFinished, + this, &BuchungenModel::getBuchungenFinished); + + return true; +} + +void BuchungenModel::getBuchungenFinished(bool success, const QString &message, const QVector &buchungen) +{ + disconnect(&m_erfassung, &Zeiterfassung::getBuchungenFinished, + this, &BuchungenModel::getBuchungenFinished); + + if(success) + { + beginResetModel(); + m_buchungen = buchungen; + endResetModel(); + } + + Q_EMIT refreshFinished(success, message); +} diff --git a/models/buchungenmodel.h b/models/buchungenmodel.h new file mode 100644 index 0000000..115b1af --- /dev/null +++ b/models/buchungenmodel.h @@ -0,0 +1,68 @@ +#ifndef BUCHUNGENMODEL_H +#define BUCHUNGENMODEL_H + +#include +#include + +#include "zeiterfassung.h" + +class BuchungenModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit BuchungenModel(Zeiterfassung &erfassung, QObject *parent = nullptr); + + // QAbstractItemModel interface + int rowCount(const QModelIndex &parent) const Q_DECL_OVERRIDE; + int columnCount(const QModelIndex &parent) const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, int role) const Q_DECL_OVERRIDE; + + Zeiterfassung::Buchung getBuchung(const QModelIndex &index) const; + + bool refresh(int userId, const QDate &from, const QDate &to); + + typedef QVector::iterator iterator; + typedef QVector::const_iterator const_iterator; + typedef QVector::reverse_iterator reverse_iterator; + typedef QVector::const_reverse_iterator const_reverse_iterator; + +#if !defined(QT_STRICT_ITERATORS) || defined(Q_QDOC) + //inline iterator begin() { return m_buchungen.begin(); } + inline const_iterator begin() const Q_DECL_NOTHROW { return m_buchungen.begin(); } + inline const_iterator cbegin() const Q_DECL_NOTHROW { return m_buchungen.cbegin(); } + inline const_iterator constBegin() const Q_DECL_NOTHROW { return m_buchungen.constBegin(); } + //inline iterator end() { return m_buchungen.end(); } + inline const_iterator end() const Q_DECL_NOTHROW { return m_buchungen.end(); } + inline const_iterator cend() const Q_DECL_NOTHROW { return m_buchungen.cend(); } + inline const_iterator constEnd() const Q_DECL_NOTHROW { return m_buchungen.constEnd(); } +#else + //inline iterator begin(iterator = iterator()) { return m_buchungen.begin(); } + inline const_iterator begin(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return m_buchungen.begin(); } + inline const_iterator cbegin(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return d->constBegin(); } + inline const_iterator constBegin(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return m_buchungen.constBegin(); } + //inline iterator end(iterator = iterator()) { return m_buchungen.end(); } + inline const_iterator end(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return m_buchungen.end(); } + inline const_iterator cend(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return m_buchungen.cend(); } + inline const_iterator constEnd(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return m_buchungen.constEnd(); } +#endif + //reverse_iterator rbegin() { return m_buchungen.rbegin(); } + //reverse_iterator rend() { return m_buchungen.rbegin(); } + const_reverse_iterator rbegin() const Q_DECL_NOTHROW { return m_buchungen.rbegin(); } + const_reverse_iterator rend() const Q_DECL_NOTHROW { return m_buchungen.rend(); } + const_reverse_iterator crbegin() const Q_DECL_NOTHROW { return m_buchungen.crbegin(); } + const_reverse_iterator crend() const Q_DECL_NOTHROW { return m_buchungen.crend(); } + +Q_SIGNALS: + void refreshFinished(bool success, const QString &message); + +private Q_SLOTS: + void getBuchungenFinished(bool success, const QString &message, const QVector &buchungen); + +private: + Zeiterfassung &m_erfassung; + QVector m_buchungen; +}; + +#endif // BUCHUNGENMODEL_H diff --git a/models/kontierungenmodel.cpp b/models/kontierungenmodel.cpp new file mode 100644 index 0000000..7ba5194 --- /dev/null +++ b/models/kontierungenmodel.cpp @@ -0,0 +1,110 @@ +#include "kontierungenmodel.h" + +KontierungenModel::KontierungenModel(Zeiterfassung &erfassung, QObject *parent) : + QAbstractListModel(parent), + m_erfassung(erfassung) +{ + +} + +int KontierungenModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return m_kontierungen.count(); +} + +int KontierungenModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return 7; +} + +QVariant KontierungenModel::data(const QModelIndex &index, int role) const +{ + Q_ASSERT(index.row() < m_kontierungen.count()); + const auto &kontierung = m_kontierungen.at(index.row()); + + switch(role) + { + case Qt::DisplayRole: + case Qt::EditRole: + switch(index.column()) + { + case 0: return kontierung.id; + case 1: return kontierung.time; + case 2: return kontierung.timespan; + case 3: return kontierung.projekt; + case 4: return kontierung.subprojekt; + case 5: return kontierung.workpackage; + case 6: return kontierung.text; + } + } + + return QVariant(); +} + +QVariant KontierungenModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch(orientation) + { + case Qt::Horizontal: + switch(role) + { + case Qt::DisplayRole: + case Qt::EditRole: + switch(section) + { + case 0: return tr("ID"); + case 1: return tr("Time"); + case 2: return tr("Timespan"); + case 3: return tr("Projekt"); + case 4: return tr("Subprojekt"); + case 5: return tr("Workpackage"); + case 6: return tr("Text"); + } + } + } + + return QVariant(); +} + +Zeiterfassung::Kontierung KontierungenModel::getKontierung(const QModelIndex &index) const +{ + if(!index.isValid()) + return Zeiterfassung::Kontierung(); + + Q_ASSERT(index.row() <= m_kontierungen.count()); + return m_kontierungen.at(index.row()); +} + +bool KontierungenModel::refresh(int userId, const QDate &from, const QDate &to) +{ + if(!m_erfassung.doGetKontierungen(userId, from, to)) + return false; + + beginResetModel(); + m_kontierungen.clear(); + endResetModel(); + + connect(&m_erfassung, &Zeiterfassung::getKontierungenFinished, + this, &KontierungenModel::getKontierungenFinished); + + return true; +} + +void KontierungenModel::getKontierungenFinished(bool success, const QString &message, const QVector &kontierungen) +{ + disconnect(&m_erfassung, &Zeiterfassung::getKontierungenFinished, + this, &KontierungenModel::getKontierungenFinished); + + if(success) + { + beginResetModel(); + m_kontierungen = kontierungen; + endResetModel(); + } + + Q_EMIT refreshFinished(success, message); +} diff --git a/models/kontierungenmodel.h b/models/kontierungenmodel.h new file mode 100644 index 0000000..a666025 --- /dev/null +++ b/models/kontierungenmodel.h @@ -0,0 +1,68 @@ +#ifndef KONTIERUNGENMODEL_H +#define KONTIERUNGENMODEL_H + +#include +#include + +#include "zeiterfassung.h" + +class KontierungenModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit KontierungenModel(Zeiterfassung &erfassung, QObject *parent = nullptr); + + // QAbstractItemModel interface + int rowCount(const QModelIndex &parent) const Q_DECL_OVERRIDE; + int columnCount(const QModelIndex &parent) const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, int role) const Q_DECL_OVERRIDE; + + Zeiterfassung::Kontierung getKontierung(const QModelIndex &index) const; + + bool refresh(int userId, const QDate &from, const QDate &to); + + typedef QVector::iterator iterator; + typedef QVector::const_iterator const_iterator; + typedef QVector::reverse_iterator reverse_iterator; + typedef QVector::const_reverse_iterator const_reverse_iterator; + +#if !defined(QT_STRICT_ITERATORS) || defined(Q_QDOC) + //inline iterator begin() { return m_kontierungen.begin(); } + inline const_iterator begin() const Q_DECL_NOTHROW { return m_kontierungen.begin(); } + inline const_iterator cbegin() const Q_DECL_NOTHROW { return m_kontierungen.cbegin(); } + inline const_iterator constBegin() const Q_DECL_NOTHROW { return m_kontierungen.constBegin(); } + //inline iterator end() { return m_kontierungen.end(); } + inline const_iterator end() const Q_DECL_NOTHROW { return m_kontierungen.end(); } + inline const_iterator cend() const Q_DECL_NOTHROW { return m_kontierungen.cend(); } + inline const_iterator constEnd() const Q_DECL_NOTHROW { return m_kontierungen.constEnd(); } +#else + //inline iterator begin(iterator = iterator()) { return m_kontierungen.begin(); } + inline const_iterator begin(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return m_kontierungen.begin(); } + inline const_iterator cbegin(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return d->constBegin(); } + inline const_iterator constBegin(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return m_kontierungen.constBegin(); } + //inline iterator end(iterator = iterator()) { return m_kontierungen.end(); } + inline const_iterator end(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return m_kontierungen.end(); } + inline const_iterator cend(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return m_kontierungen.cend(); } + inline const_iterator constEnd(const_iterator = const_iterator()) const Q_DECL_NOTHROW { return m_kontierungen.constEnd(); } +#endif + //reverse_iterator rbegin() { return m_kontierungen.rbegin(); } + //reverse_iterator rend() { return m_kontierungen.rbegin(); } + const_reverse_iterator rbegin() const Q_DECL_NOTHROW { return m_kontierungen.rbegin(); } + const_reverse_iterator rend() const Q_DECL_NOTHROW { return m_kontierungen.rend(); } + const_reverse_iterator crbegin() const Q_DECL_NOTHROW { return m_kontierungen.crbegin(); } + const_reverse_iterator crend() const Q_DECL_NOTHROW { return m_kontierungen.crend(); } + +Q_SIGNALS: + void refreshFinished(bool success, const QString &message); + +private Q_SLOTS: + void getKontierungenFinished(bool success, const QString &message, const QVector &kontierungen); + +private: + Zeiterfassung &m_erfassung; + QVector m_kontierungen; +}; + +#endif // KONTIERUNGENMODEL_H diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..967df7a --- /dev/null +++ b/resources.qrc @@ -0,0 +1,11 @@ + + + images/splash.png + images/authentication.png + images/next.png + images/previous.png + images/quit.png + images/refresh.png + images/today.png + + diff --git a/zeiterfassung.conf b/zeiterfassung.conf new file mode 100644 index 0000000..36a5d44 --- /dev/null +++ b/zeiterfassung.conf @@ -0,0 +1,8 @@ +[General] +password=HAHA +projekte=0000001142, 0000010001, SONSTIGES +subprojekte= +texte= +url=http://localhost:8080/evoApps/ +username=danielb +workpackages=[D.1315], [M.0200] diff --git a/zeiterfassung.cpp b/zeiterfassung.cpp new file mode 100644 index 0000000..877943e --- /dev/null +++ b/zeiterfassung.cpp @@ -0,0 +1,817 @@ +#include "zeiterfassung.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +QString QJsonValue::toString() const{ return toString(QString()); } + +Zeiterfassung::Zeiterfassung(const QString &url, QObject *parent) : + QObject(parent), + m_url(url), + m_manager(new QNetworkAccessManager(this)), + m_replies { Q_NULLPTR, Q_NULLPTR, Q_NULLPTR, Q_NULLPTR, Q_NULLPTR, Q_NULLPTR, + Q_NULLPTR, Q_NULLPTR, Q_NULLPTR, Q_NULLPTR, Q_NULLPTR, Q_NULLPTR } +{ +} + +const QString &Zeiterfassung::url() const +{ + return m_url; +} + +void Zeiterfassung::setUrl(const QString &url) +{ + if(m_url != url) + Q_EMIT urlChanged(m_url = url); +} + +bool Zeiterfassung::doLoginPage() +{ + if(m_replies.login) + { + qWarning() << "another loginPage already processing!"; + return false; + } + + QNetworkRequest request(QUrl(m_url % "pages/login.jsp")); + + m_replies.loginPage = m_manager->get(request); + connect(m_replies.loginPage, &QNetworkReply::finished, this, &Zeiterfassung::loginPageRequestFinished); + + return true; +} + +bool Zeiterfassung::doLogin(const QString &username, const QString &password) +{ + if(m_replies.login) + { + qWarning() << "another login already processing!"; + return false; + } + + QNetworkRequest request(QUrl(m_url % "pages/j_spring_security_check")); + request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded")); + request.setMaximumRedirectsAllowed(0); + + m_replies.login = m_manager->post(request, QStringLiteral("j_username=%0&j_password=%1&login=Anmelden").arg(username).arg(password).toUtf8()); + connect(m_replies.login, &QNetworkReply::finished, this, &Zeiterfassung::loginRequestFinished); + + return true; +} + +bool Zeiterfassung::doUserInfo() +{ + if(m_replies.userInfo) + { + qWarning() << "another userInfo already processing!"; + return false; + } + + QNetworkRequest request(QUrl(m_url % "json/evoAppsUserInfoDialogController/load-EvoAppsUserInfoTO")); + request.setRawHeader(QByteArrayLiteral("sisAppName"), QByteArrayLiteral("home")); + + m_replies.userInfo = m_manager->get(request); + connect(m_replies.userInfo, &QNetworkReply::finished, this, &Zeiterfassung::userInfoRequestFinished); + + return true; +} + +bool Zeiterfassung::doGetBuchungen(int userId, const QDate &start, const QDate &end) +{ + if(m_replies.getBuchungen) + { + qWarning() << "another getBuchungen already processing!"; + return false; + } + + QNetworkRequest request(QUrl(QStringLiteral("%0json/bookings?start=%1&end=%2&pnrLst=%3") + .arg(m_url) + .arg(start.toString(QStringLiteral("yyyyMMdd"))) + .arg(end.toString(QStringLiteral("yyyyMMdd"))) + .arg(userId))); + request.setRawHeader(QByteArrayLiteral("sisAppName"), QByteArrayLiteral("bookingCalendar")); + + m_replies.getBuchungen = m_manager->get(request); + connect(m_replies.getBuchungen, &QNetworkReply::finished, + this, &Zeiterfassung::getBuchungenRequestFinished); + + return true; +} + +bool Zeiterfassung::doCreateBuchung(int userId, const QDate &date, const QTime &time, const QTime ×pan, const QString &type, const QString &text) +{ + if(m_replies.createBuchung) + { + qWarning() << "another createBuchung already processing!"; + return false; + } + + QNetworkRequest request(QUrl(m_url % "json/booking")); + request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/json")); + request.setRawHeader(QByteArrayLiteral("sisAppName"), QByteArrayLiteral("bookingCalendar")); + + QJsonObject obj; + obj[QStringLiteral("persNr")] = userId; + obj[QStringLiteral("bookingDate")] = date.toString("yyyyMMdd").toInt(); + obj[QStringLiteral("bookingTime")] = time.toString("Hmmss").toInt(); + obj[QStringLiteral("bookingTimespan")] = timespan.toString("Hmmss").toInt(); + obj[QStringLiteral("bookingType")] = type; + obj[QStringLiteral("hourCategory")] = QStringLiteral(""); + obj[QStringLiteral("empfEinh")] = QStringLiteral(""); + obj[QStringLiteral("bewEinh")] = QStringLiteral(""); + obj[QStringLiteral("text")] = text; + + m_replies.createBuchung = m_manager->post(request, QJsonDocument(obj).toJson()); + connect(m_replies.createBuchung, &QNetworkReply::finished, + this, &Zeiterfassung::createBuchungRequestFinished); + + return true; +} + +bool Zeiterfassung::doUpdateBuchung(int buchungId, int userId, const QDate &date, const QTime &time, const QTime ×pan, const QString &type, const QString &text) +{ + if(m_replies.updateBuchung) + { + qWarning() << "another updateBuchung already processing!"; + return false; + } + + QNetworkRequest request(QUrl(QStringLiteral("%0json/booking/%1").arg(m_url).arg(buchungId))); + request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/json")); + request.setRawHeader(QByteArrayLiteral("sisAppName"), QByteArrayLiteral("bookingCalendar")); + + QJsonObject obj; + obj[QStringLiteral("bookingNr")] = buchungId; + obj[QStringLiteral("persNr")] = userId; + obj[QStringLiteral("bookingDate")] = date.toString("yyyyMMdd").toInt(); + obj[QStringLiteral("bookingTime")] = time.toString("Hmmss").toInt(); + obj[QStringLiteral("bookingTimespan")] = timespan.toString("Hmmss").toInt(); + obj[QStringLiteral("bookingType")] = type; + obj[QStringLiteral("hourCategory")] = QStringLiteral(""); + obj[QStringLiteral("empfEinh")] = QStringLiteral(""); + obj[QStringLiteral("bewEinh")] = QStringLiteral(""); + obj[QStringLiteral("text")] = text; + + m_replies.updateBuchung = m_manager->put(request, QJsonDocument(obj).toJson()); + connect(m_replies.updateBuchung, &QNetworkReply::finished, + this, &Zeiterfassung::updateBuchungRequestFinished); + + return true; +} + +bool Zeiterfassung::doDeleteBuchung(int buchungId) +{ + if(m_replies.deleteBuchung) + { + qWarning() << "another deleteBuchung already processing!"; + return false; + } + + QNetworkRequest request(QUrl(QStringLiteral("%0json/booking/%1?text=") + .arg(m_url) + .arg(buchungId))); + request.setRawHeader(QByteArrayLiteral("sisAppName"), QByteArrayLiteral("bookingCalendar")); + + m_replies.deleteBuchung = m_manager->deleteResource(request); + connect(m_replies.deleteBuchung, &QNetworkReply::finished, + this, &Zeiterfassung::deleteBuchungRequestFinished); + + return true; +} + +bool Zeiterfassung::doGetKontierungen(int userId, const QDate &start, const QDate &end) +{ + if(m_replies.getKontierungen) + { + qWarning() << "another getKontierungen already processing!"; + return false; + } + + QNetworkRequest request(QUrl(QStringLiteral("%0json/azebooking?start=%1&end=%2&pnrLst=%3") + .arg(m_url) + .arg(start.toString(QStringLiteral("yyyyMMdd"))) + .arg(end.toString(QStringLiteral("yyyyMMdd"))) + .arg(userId))); + request.setRawHeader(QByteArrayLiteral("sisAppName"), QByteArrayLiteral("bookingCalendar")); + + m_replies.getKontierungen = m_manager->get(request); + connect(m_replies.getKontierungen, &QNetworkReply::finished, + this, &Zeiterfassung::getKontierungenRequestFinished); + + return true; +} + +bool Zeiterfassung::doCreateKontierung(int userId, const QDate &date, const QTime &time, const QTime ×pan, const QString &projekt, const QString &subprojekt, const QString &workpackage, const QString &text) +{ + if(m_replies.createKontierung) + { + qWarning() << "another createKontierung already processing!"; + return false; + } + + QNetworkRequest request(QUrl(m_url % "json/azebooking")); + request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/json")); + request.setRawHeader(QByteArrayLiteral("sisAppName"), QByteArrayLiteral("bookingCalendar")); + + QJsonObject obj; + obj[QStringLiteral("bookingNr")] = QJsonValue::Null; + obj[QStringLiteral("persNr")] = userId; + obj[QStringLiteral("bookingDate")] = date.toString("yyyyMMdd").toInt(); + obj[QStringLiteral("bookingTime")] = time.toString("Hmmss").toInt(); + obj[QStringLiteral("bookingTimespan")] = timespan.toString("Hmmss").toInt(); + obj[QStringLiteral("text")] = text; + { + QJsonArray koWertList; + { + QJsonObject obj; + obj[QStringLiteral("value")] = projekt; + koWertList << obj; + } + { + QJsonObject obj; + obj[QStringLiteral("value")] = subprojekt; + koWertList << obj; + } + { + QJsonObject obj; + obj[QStringLiteral("value")] = workpackage; + koWertList << obj; + } + obj[QStringLiteral("koWertList")] = koWertList; + } + + m_replies.createKontierung = m_manager->post(request, QJsonDocument(obj).toJson()); + connect(m_replies.createKontierung, &QNetworkReply::finished, + this, &Zeiterfassung::createKontierungRequestFinished); + + return true; +} + +bool Zeiterfassung::doUpdateKontierung(int kontierungId, int userId, const QDate &date, const QTime &time, const QTime ×pan, const QString &projekt, const QString &subprojekt, const QString &workpackage, const QString &text) +{ + if(m_replies.updateKontierung) + { + qWarning() << "another updateKontierung already processing!"; + return false; + } + + QNetworkRequest request(QUrl(QStringLiteral("%0json/azebooking/%1").arg(m_url).arg(kontierungId))); + request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/json")); + request.setRawHeader(QByteArrayLiteral("sisAppName"), QByteArrayLiteral("bookingCalendar")); + + QJsonObject obj; + obj[QStringLiteral("bookingNr")] = kontierungId; + obj[QStringLiteral("persNr")] = userId; + obj[QStringLiteral("bookingDate")] = date.toString("yyyyMMdd").toInt(); + obj[QStringLiteral("bookingTime")] = time.toString("Hmmss").toInt(); + obj[QStringLiteral("bookingTimespan")] = timespan.toString("Hmmss").toInt(); + obj[QStringLiteral("bookingType")] = QJsonValue::Null; + obj[QStringLiteral("hourCategory")] = QJsonValue::Null; + obj[QStringLiteral("bewEinh")] = QJsonValue::Null; + obj[QStringLiteral("empfEinh")] = QJsonValue::Null; + obj[QStringLiteral("einstuf")] = 0; + obj[QStringLiteral("text")] = text; + { + QJsonArray koWertList; + { + QJsonObject obj; + obj[QStringLiteral("value")] = projekt; + koWertList << obj; + } + { + QJsonObject obj; + obj[QStringLiteral("value")] = subprojekt; + koWertList << obj; + } + { + QJsonObject obj; + obj[QStringLiteral("value")] = workpackage; + koWertList << obj; + } + obj[QStringLiteral("koWertList")] = koWertList; + } + + m_replies.updateKontierung = m_manager->put(request, QJsonDocument(obj).toJson()); + connect(m_replies.updateKontierung, &QNetworkReply::finished, + this, &Zeiterfassung::updateKontierungRequestFinished); + + return true; +} + +bool Zeiterfassung::doDeleteKontierung(int kontierungId) +{ + if(m_replies.deleteKontierung) + { + qWarning() << "another deleteKontierung already processing!"; + return false; + } + + QNetworkRequest request(QUrl(QStringLiteral("%0json/azebooking/%1") + .arg(m_url) + .arg(kontierungId))); + request.setRawHeader(QByteArrayLiteral("sisAppName"), QByteArrayLiteral("bookingCalendar")); + + m_replies.deleteKontierung = m_manager->deleteResource(request); + connect(m_replies.deleteKontierung, &QNetworkReply::finished, + this, &Zeiterfassung::deleteKontierungRequestFinished); + + return true; +} + +bool Zeiterfassung::doGetProjekte(int userId, QDate date) +{ + if(m_replies.getProjekte) + { + qWarning() << "another getProjekte already processing!"; + return false; + } + + QNetworkRequest request(QUrl(QStringLiteral("%0json/combobox?persnr=%1&date=%2&dqkey=KOST&kowert0=&kowert1=&kowert2=&term=") + .arg(m_url) + .arg(userId) + .arg(date.toString(QStringLiteral("yyyyMMdd"))))); + request.setRawHeader(QByteArrayLiteral("sisAppName"), QByteArrayLiteral("bookingCalendar")); + + m_replies.getProjekte = m_manager->get(request); + connect(m_replies.getProjekte, &QNetworkReply::finished, this, &Zeiterfassung::getProjekteRequestFinished); + + return true; +} + +void Zeiterfassung::loginPageRequestFinished() +{ + if(m_replies.loginPage->error() != QNetworkReply::NoError) + { + Q_EMIT loginPageFinished(false, tr("Request error occured: %0").arg(m_replies.loginPage->error())); + goto end; + } + + if(!m_replies.loginPage->readAll().contains(QByteArrayLiteral("evoApps Anmeldung"))) + { + Q_EMIT loginPageFinished(false, tr("Could not find necessary keywords in login page!")); + goto end; + } + + Q_EMIT loginPageFinished(true, QString()); + + end: + m_replies.loginPage->deleteLater(); + m_replies.loginPage = Q_NULLPTR; +} + +void Zeiterfassung::loginRequestFinished() +{ + if(m_replies.login->error() != QNetworkReply::NoError) + { + Q_EMIT loginFinished(false, tr("Request error occured: %0").arg(m_replies.login->error())); + goto end; + } + + if(!m_replies.login->hasRawHeader(QByteArrayLiteral("Location"))) + { + Q_EMIT loginFinished(false, tr("Request did not contain a Location header.")); + goto end; + } + + { + auto location = m_replies.login->rawHeader(QByteArrayLiteral("Location")); + + if(location == QByteArrayLiteral("/evoApps/pages/home.jsp")) + { + Q_EMIT loginFinished(true, QString()); + goto end; + } + else if(location == QByteArrayLiteral("/evoApps/pages/login.jsp?error=user")) + { + Q_EMIT loginFinished(false, tr("Authentication failure. Please check username and password.")); + goto end; + } + else + { + Q_EMIT loginFinished(false, tr("An unknown authentication failure occured. Redirected to: %0").arg(QString(location))); + goto end; + } + } + + end: + m_replies.login->deleteLater(); + m_replies.login = Q_NULLPTR; +} + +void Zeiterfassung::userInfoRequestFinished() +{ + if(m_replies.userInfo->error() != QNetworkReply::NoError) + { + Q_EMIT userInfoFinished(false, tr("Request error occured: %0").arg(m_replies.userInfo->error()), UserInfo()); + goto end; + } + + { + QJsonParseError error; + auto document = QJsonDocument::fromJson(m_replies.userInfo->readAll(), &error); + if(error.error != QJsonParseError::NoError) + { + Q_EMIT userInfoFinished(false, tr("Parsing JSON failed: %0").arg(error.errorString()), UserInfo()); + goto end; + } + + if(!document.isObject()) + { + Q_EMIT userInfoFinished(false, tr("JSON document is not an object!"), UserInfo()); + goto end; + } + + auto rootObj = document.object(); + + if(!rootObj.contains(QStringLiteral("evoAppsUser"))) + { + Q_EMIT userInfoFinished(false, tr("JSON does not contain evoAppsUser!"), UserInfo()); + goto end; + } + + auto evoAppsUser = rootObj.value(QStringLiteral("evoAppsUser")); + + if(!evoAppsUser.isObject()) + { + Q_EMIT userInfoFinished(false, tr("evoAppsUser is not an object!"), UserInfo()); + goto end; + } + + auto evoAppsUserObj = evoAppsUser.toObject(); + + Q_EMIT userInfoFinished(true, QString(), { + evoAppsUserObj.value(QStringLiteral("persNr")).toInt(), + evoAppsUserObj.value(QStringLiteral("email")).toString(), + evoAppsUserObj.value(QStringLiteral("longUsername")).toString(), + evoAppsUserObj.value(QStringLiteral("text")).toString(), + evoAppsUserObj.value(QStringLiteral("username")).toString() + }); + } + + end: + m_replies.userInfo->deleteLater(); + m_replies.userInfo = Q_NULLPTR; +} + +void Zeiterfassung::getBuchungenRequestFinished() +{ + if(m_replies.getBuchungen->error() != QNetworkReply::NoError) + { + Q_EMIT getBuchungenFinished(false, tr("Request error occured: %0").arg(m_replies.getBuchungen->error()), {}); + goto end; + } + + { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(m_replies.getBuchungen->readAll(), &error); + if(error.error != QJsonParseError::NoError) + { + Q_EMIT getBuchungenFinished(false, tr("Parsing JSON failed: %0").arg(error.errorString()), {}); + goto end; + } + + if(!document.isArray()) + { + Q_EMIT getBuchungenFinished(false, tr("JSON document is not an array!"), {}); + goto end; + } + + auto arr = document.array(); + QVector buchungen; + + for(const auto &val : arr) + { + auto obj = val.toObject(); + + buchungen.append({ + obj.value(QStringLiteral("bookingNr")).toInt(), + QDate::fromString(QString::number(obj.value(QStringLiteral("bookingDate")).toInt()), QStringLiteral("yyyyMMdd")), + QTime::fromString(QString("%0").arg(obj.value(QStringLiteral("bookingTime")).toInt(), 6, 10, QChar('0')), QStringLiteral("HHmmss")), + QTime::fromString(QString("%0").arg(obj.value(QStringLiteral("bookingTimespan")).toInt(), 6, 10, QChar('0')), QStringLiteral("HHmmss")), + obj.value(QStringLiteral("bookingType")).toString(), + obj.value(QStringLiteral("text")).toString() + }); + } + + Q_EMIT getBuchungenFinished(true, QString(), buchungen); + } + + end: + m_replies.getBuchungen->deleteLater(); + m_replies.getBuchungen = Q_NULLPTR; +} + +void Zeiterfassung::createBuchungRequestFinished() +{ + if(m_replies.createBuchung->error() != QNetworkReply::NoError) + { + Q_EMIT createBuchungFinished(false, tr("Request error occured: %0").arg(m_replies.createBuchung->error()), -1); + goto end; + } + + { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(m_replies.createBuchung->readAll(), &error); + if(error.error != QJsonParseError::NoError) + { + Q_EMIT createBuchungFinished(false, tr("Parsing JSON failed: %0").arg(error.errorString()), -1); + goto end; + } + + if(!document.isObject()) + { + Q_EMIT createBuchungFinished(false, tr("JSON document is not an object!"), -1); + goto end; + } + + auto obj = document.object(); + + if(!obj.contains(QStringLiteral("bookingNr"))) + { + Q_EMIT createBuchungFinished(false, tr("JSON does not contain bookingNr!"), -1); + goto end; + } + + auto buchungId = obj.value(QStringLiteral("bookingNr")).toInt(); + + Q_EMIT createBuchungFinished(true, QString(), buchungId); + } + + end: + m_replies.createBuchung->deleteLater(); + m_replies.createBuchung = Q_NULLPTR; +} + +void Zeiterfassung::updateBuchungRequestFinished() +{ + if(m_replies.updateBuchung->error() != QNetworkReply::NoError) + { + Q_EMIT updateBuchungFinished(false, tr("Request error occured: %0").arg(m_replies.updateBuchung->error()), -1); + goto end; + } + + { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(m_replies.updateBuchung->readAll(), &error); + if(error.error != QJsonParseError::NoError) + { + Q_EMIT updateBuchungFinished(false, tr("Parsing JSON failed: %0").arg(error.errorString()), -1); + goto end; + } + + if(!document.isObject()) + { + Q_EMIT updateBuchungFinished(false, tr("JSON document is not an object!"), -1); + goto end; + } + + auto obj = document.object(); + + if(!obj.contains(QStringLiteral("bookingNr"))) + { + Q_EMIT updateBuchungFinished(false, tr("JSON does not contain bookingNr!"), -1); + goto end; + } + + auto buchungId = obj.value(QStringLiteral("bookingNr")).toInt(); + + Q_EMIT updateBuchungFinished(true, QString(), buchungId); + } + + end: + m_replies.updateBuchung->deleteLater(); + m_replies.updateBuchung = Q_NULLPTR; +} + +void Zeiterfassung::deleteBuchungRequestFinished() +{ + if(m_replies.deleteBuchung->error() != QNetworkReply::NoError) + { + Q_EMIT deleteBuchungFinished(false, tr("Request error occured: %0").arg(m_replies.deleteBuchung->error())); + goto end; + } + + //should be empty, so nothing to check... + Q_EMIT deleteBuchungFinished(true, QString()); + + end: + m_replies.deleteBuchung->deleteLater(); + m_replies.deleteBuchung = Q_NULLPTR; +} + +void Zeiterfassung::getKontierungenRequestFinished() +{ + if(m_replies.getKontierungen->error() != QNetworkReply::NoError) + { + Q_EMIT getKontierungenFinished(false, tr("Request error occured: %0").arg(m_replies.getKontierungen->error()), {}); + goto end; + } + + { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(m_replies.getKontierungen->readAll(), &error); + if(error.error != QJsonParseError::NoError) + { + Q_EMIT getKontierungenFinished(false, tr("Parsing JSON failed: %0").arg(error.errorString()), {}); + goto end; + } + + if(!document.isArray()) + { + Q_EMIT getKontierungenFinished(false, tr("JSON document is not an array!"), {}); + goto end; + } + + auto arr = document.array(); + QVector kontierungen; + + for(const auto &val : arr) + { + auto obj = val.toObject(); + + auto koWertList = obj.value(QStringLiteral("koWertList")).toArray(); + + kontierungen.append({ + obj.value(QStringLiteral("bookingNr")).toInt(), + QDate::fromString(QString::number(obj.value(QStringLiteral("bookingDate")).toInt()), QStringLiteral("yyyyMMdd")), + QTime::fromString(QString("%0").arg(obj.value(QStringLiteral("bookingTime")).toInt(), 6, 10, QChar('0')), QStringLiteral("HHmmss")), + QTime::fromString(QString("%0").arg(obj.value(QStringLiteral("bookingTimespan")).toInt(), 6, 10, QChar('0')), QStringLiteral("HHmmss")), + obj.value(QStringLiteral("text")).toString(), + koWertList.at(0).toObject().value(QStringLiteral("value")).toString(), + koWertList.at(1).toObject().value(QStringLiteral("value")).toString(), + koWertList.at(2).toObject().value(QStringLiteral("value")).toString() + }); + } + + Q_EMIT getKontierungenFinished(true, QString(), kontierungen); + } + + end: + m_replies.getKontierungen->deleteLater(); + m_replies.getKontierungen = Q_NULLPTR; +} + +void Zeiterfassung::createKontierungRequestFinished() +{ + if(m_replies.createKontierung->error() != QNetworkReply::NoError) + { + Q_EMIT createKontierungFinished(false, tr("Request error occured: %0").arg(m_replies.createKontierung->error()), -1); + goto end; + } + + { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(m_replies.createKontierung->readAll(), &error); + if(error.error != QJsonParseError::NoError) + { + Q_EMIT createKontierungFinished(false, tr("Parsing JSON failed: %0").arg(error.errorString()), -1); + goto end; + } + + if(!document.isObject()) + { + Q_EMIT createKontierungFinished(false, tr("JSON document is not an object!"), -1); + goto end; + } + + auto obj = document.object(); + + if(!obj.contains(QStringLiteral("bookingNr"))) + { + Q_EMIT createKontierungFinished(false, tr("JSON does not contain bookingNr!"), -1); + goto end; + } + + auto kontierungId = obj.value(QStringLiteral("bookingNr")).toInt(); + + Q_EMIT createKontierungFinished(true, QString(), kontierungId); + } + + end: + m_replies.createKontierung->deleteLater(); + m_replies.createKontierung = Q_NULLPTR; +} + +void Zeiterfassung::updateKontierungRequestFinished() +{ + if(m_replies.updateKontierung->error() != QNetworkReply::NoError) + { + Q_EMIT updateKontierungFinished(false, tr("Request error occured: %0").arg(m_replies.updateKontierung->error()), -1); + goto end; + } + + { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(m_replies.updateKontierung->readAll(), &error); + if(error.error != QJsonParseError::NoError) + { + Q_EMIT updateKontierungFinished(false, tr("Parsing JSON failed: %0").arg(error.errorString()), -1); + goto end; + } + + if(!document.isObject()) + { + Q_EMIT updateKontierungFinished(false, tr("JSON document is not an object!"), 0); + goto end; + } + + auto obj = document.object(); + + if(!obj.contains(QStringLiteral("bookingNr"))) + { + Q_EMIT updateKontierungFinished(false, tr("JSON does not contain bookingNr!"), 0); + goto end; + } + + auto kontierungId = obj.value(QStringLiteral("bookingNr")).toInt(); + + Q_EMIT updateKontierungFinished(true, QString(), kontierungId); + } + + end: + m_replies.updateKontierung->deleteLater(); + m_replies.updateKontierung = Q_NULLPTR; +} + +void Zeiterfassung::deleteKontierungRequestFinished() +{ + if(m_replies.deleteKontierung->error() != QNetworkReply::NoError) + { + Q_EMIT deleteKontierungFinished(false, tr("Request error occured: %0").arg(m_replies.deleteKontierung->error())); + goto end; + } + + //only contains deleted id, so nothing to check here + Q_EMIT deleteKontierungFinished(true, QString()); + + end: + m_replies.deleteKontierung->deleteLater(); + m_replies.deleteKontierung = Q_NULLPTR; +} + +void Zeiterfassung::getProjekteRequestFinished() +{ + if(m_replies.getProjekte->error() != QNetworkReply::NoError) + { + Q_EMIT getProjekteFinished(false, tr("Request error occured: %0").arg(m_replies.getProjekte->error()), {}); + goto end; + } + + { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(m_replies.getProjekte->readAll(), &error); + if(error.error != QJsonParseError::NoError) + { + Q_EMIT getProjekteFinished(false, tr("Parsing JSON failed: %0").arg(error.errorString()), {}); + goto end; + } + + if(!document.isObject()) + { + Q_EMIT getProjekteFinished(false, tr("JSON document is not an object!"), {}); + goto end; + } + + auto rootObj = document.object(); + + if(!rootObj.contains(QStringLiteral("elements"))) + { + Q_EMIT getProjekteFinished(false, tr("JSON does not contain elements!"), {}); + goto end; + } + + auto elements = rootObj.value(QStringLiteral("elements")); + + if(!elements.isArray()) + { + Q_EMIT getProjekteFinished(false, tr("elements is not an array!"), {}); + goto end; + } + + auto elementsArr = elements.toArray(); + QVector projekte; + + for(const auto &val : elementsArr) + { + auto obj = val.toObject(); + + projekte.append({ + obj.value(QStringLiteral("label")).toString(), + obj.value(QStringLiteral("value")).toString() + }); + } + + Q_EMIT getProjekteFinished(true, QString(), projekte); + } + + end: + m_replies.getProjekte->deleteLater(); + m_replies.getProjekte = Q_NULLPTR; +} diff --git a/zeiterfassung.h b/zeiterfassung.h new file mode 100644 index 0000000..5a668f4 --- /dev/null +++ b/zeiterfassung.h @@ -0,0 +1,142 @@ +#ifndef ZEITERFASSUNG_H +#define ZEITERFASSUNG_H + +#include +#include +#include +#include + +class QNetworkAccessManager; +class QNetworkReply; + +class Zeiterfassung : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString url READ url WRITE setUrl NOTIFY urlChanged) + +public: + explicit Zeiterfassung(const QString &url, QObject *parent = 0); + + const QString &url() const; + void setUrl(const QString &url); + + struct UserInfo + { + int userId; + QString email; + QString longUsername; + QString text; + QString username; + }; + + struct Buchung + { + int id; + QDate date; + QTime time; + QTime timespan; + QString type; + QString text; + }; + + struct Kontierung + { + int id; + QDate date; + QTime time; + QTime timespan; + QString text; + QString projekt; + QString subprojekt; + QString workpackage; + }; + + struct Projekt + { + QString label; + QString value; + }; + +public Q_SLOTS: + bool doLoginPage(); + bool doLogin(const QString &username, const QString &password); + bool doUserInfo(); + + bool doGetBuchungen(int userId, const QDate &start, const QDate &end); + bool doCreateBuchung(int userId, const QDate &date, const QTime &time, const QTime ×pan, + const QString &type, const QString &text); + bool doUpdateBuchung(int buchungId, int userId, const QDate &date, const QTime &time, + const QTime ×pan, const QString &type, const QString &text); + bool doDeleteBuchung(int buchungId); + + bool doGetKontierungen(int userId, const QDate &start, const QDate &end); + bool doCreateKontierung(int userId, const QDate &date, const QTime &time, const QTime ×pan, + const QString &projekt, const QString &subprojekt, const QString &workpackage, + const QString &text); + bool doUpdateKontierung(int kontierungId, int userId, const QDate &date, const QTime &time, + const QTime ×pan, const QString &projekt, const QString &subprojekt, + const QString &workpackage, const QString &text); + bool doDeleteKontierung(int kontierungId); + + bool doGetProjekte(int userId, QDate date = QDate::currentDate()); + +Q_SIGNALS: + void urlChanged(const QString &url); + + void loginPageFinished(bool success, const QString &message); + void loginFinished(bool success, const QString &message); + void userInfoFinished(bool success, const QString &message, const UserInfo &userInfo); + + void getBuchungenFinished(bool success, const QString &message, const QVector &buchungen); + void createBuchungFinished(bool success, const QString &message, int buchungId); + void updateBuchungFinished(bool success, const QString &message, int buchungId); + void deleteBuchungFinished(bool success, const QString &message); + + void getKontierungenFinished(bool success, const QString &message, const QVector &kontierungen); + void createKontierungFinished(bool success, const QString &message, int buchungId); + void updateKontierungFinished(bool success, const QString &message, int buchungId); + void deleteKontierungFinished(bool success, const QString &message); + + void getProjekteFinished(bool success, const QString &message, const QVector &projekte); + +private Q_SLOTS: + void loginPageRequestFinished(); + void loginRequestFinished(); + void userInfoRequestFinished(); + + void getBuchungenRequestFinished(); + void createBuchungRequestFinished(); + void updateBuchungRequestFinished(); + void deleteBuchungRequestFinished(); + + void getKontierungenRequestFinished(); + void createKontierungRequestFinished(); + void updateKontierungRequestFinished(); + void deleteKontierungRequestFinished(); + + void getProjekteRequestFinished(); + +private: + QString m_url; + QNetworkAccessManager *m_manager; + + struct { + QNetworkReply *loginPage; + QNetworkReply *login; + QNetworkReply *userInfo; + + QNetworkReply *getBuchungen; + QNetworkReply *createBuchung; + QNetworkReply *updateBuchung; + QNetworkReply *deleteBuchung; + + QNetworkReply *getKontierungen; + QNetworkReply *createKontierung; + QNetworkReply *updateKontierung; + QNetworkReply *deleteKontierung; + + QNetworkReply *getProjekte; + } m_replies; +}; + +#endif // ZEITERFASSUNG_H diff --git a/zeiterfassung.pro b/zeiterfassung.pro new file mode 100755 index 0000000..72e140b --- /dev/null +++ b/zeiterfassung.pro @@ -0,0 +1,47 @@ +QT += network gui widgets + +CONFIG += c++11 +CONFIG -= app_bundle + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which as been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if you use deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += main.cpp \ + zeiterfassung.cpp \ + mainwindow.cpp \ + dialogs/aboutmedialog.cpp \ + dialogs/authenticationdialog.cpp \ + dialogs/buchungdialog.cpp \ + models/buchungenmodel.cpp \ + dialogs/kontierungdialog.cpp \ + models/kontierungenmodel.cpp \ + eventloopwithstatus.cpp + +HEADERS += \ + zeiterfassung.h \ + mainwindow.h \ + dialogs/aboutmedialog.h \ + dialogs/authenticationdialog.h \ + dialogs/buchungdialog.h \ + models/buchungenmodel.h \ + dialogs/kontierungdialog.h \ + models/kontierungenmodel.h \ + eventloopwithstatus.h + +RESOURCES += \ + resources.qrc + +FORMS += \ + mainwindow.ui \ + dialogs/aboutmedialog.ui \ + dialogs/authenticationdialog.ui \ + dialogs/buchungdialog.ui \ + dialogs/kontierungdialog.ui