├── doc └── f-crm.png ├── database └── template.db ├── res └── icons │ ├── f-crm.icns │ ├── f_crm.ico │ ├── activity.svg │ ├── state_defined.svg │ ├── facebook.svg │ ├── action_waiting.svg │ ├── open.svg │ ├── outgoing.svg │ ├── person.svg │ ├── incoming.svg │ ├── move_up.svg │ ├── move_down.svg │ ├── status_watching.svg │ ├── home.svg │ ├── file.svg │ ├── star.svg │ ├── nofavourite.svg │ ├── status_prospect.svg │ ├── mail.svg │ ├── action_type_task.svg │ ├── note.svg │ ├── proposal.svg │ ├── research.svg │ ├── clipboard.svg │ ├── external-link.svg │ ├── users.svg │ ├── log_general.svg │ ├── state_progress.svg │ ├── intent.svg │ ├── ch_type_web.svg │ ├── intent_type_manual.svg │ ├── ch_type_other.svg │ ├── status_candidate.svg │ ├── document.svg │ ├── phone.svg │ ├── github.svg │ ├── action_type_channel.svg │ ├── linkedin.svg │ ├── check.svg │ ├── edit.svg │ ├── action_ready.svg │ ├── favourite.svg │ ├── not_favourite.svg │ ├── status_customer.svg │ ├── action_cancelled.svg │ ├── mobile.svg │ ├── state_terminated.svg │ ├── status_shut_down.svg │ ├── request.svg │ ├── execute_action.svg │ ├── internal.svg │ ├── action_on_hold.svg │ ├── clear_filter.svg │ ├── gender_unknown.svg │ ├── action_type_meeting.svg │ ├── state_failed.svg │ ├── status_lost.svg │ ├── action_failed.svg │ ├── status_banned.svg │ ├── delete.svg │ ├── action_done.svg │ ├── state_succeeded.svg │ ├── action_blocked.svg │ ├── edit_action.svg │ ├── gender_male.svg │ ├── reddit.svg │ ├── gender_female.svg │ ├── add_action.svg │ ├── addchannel.svg │ ├── addperson.svg │ ├── updated_person.svg │ ├── edit_intent.svg │ ├── company.svg │ ├── delete_action.svg │ ├── edit_document.svg │ └── skype.svg ├── src ├── version.h ├── utility.h ├── utility.cpp ├── aboutdialog.h ├── journalproxymodel.cpp ├── release.h ├── aboutdialog.cpp ├── journalproxymodel.h ├── strategy.h ├── favoritesdialog.h ├── channel.h ├── intentproxymodel.h ├── actionproxymodel.h ├── channelproxymodel.h ├── documentproxymodel.h ├── settingsdialog.h ├── actionexecutedialog.h ├── contactproxymodel.h ├── favoritesdialog.cpp ├── actionexecutedialog.cpp ├── intentdialog.h ├── channeldialog.h ├── persondialog.h ├── actiondialog.h ├── intent.h ├── action.h ├── logging.h ├── contactproxymodel.cpp ├── database.h ├── upcomingmodel.h ├── tableviewwithdrop.h ├── contact.h ├── channelsmodel.h ├── documentdialog.h ├── intentsmodel.h ├── channelproxymodel.cpp ├── documentproxymodel.cpp ├── intentdialog.cpp ├── main.cpp ├── intentproxymodel.cpp ├── channel.cpp ├── actionsmodel.h ├── journalmodel.h ├── documentsmodel.h ├── document.h ├── contactsmodel.h ├── intent.cpp ├── settingsdialog.cpp ├── action.cpp ├── logging.cpp ├── actionproxymodel.cpp └── persondialog.cpp ├── .gitignore ├── ci └── jenkins │ ├── Dockefile.debian-testing │ ├── Dockefile.debian-stretch │ ├── Dockefile.ubuntu-bionic │ ├── Dockefile.ubuntu-xenial │ └── Dockefile.ubuntu-trusty ├── scripts ├── package-windows.bat ├── package-deb.sh ├── package-macos.sh └── package-appimage.sh ├── ui ├── favoritesdialog.ui └── actionexecutedialog.ui └── README.md /doc/f-crm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jgaa/f-crm/HEAD/doc/f-crm.png -------------------------------------------------------------------------------- /database/template.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jgaa/f-crm/HEAD/database/template.db -------------------------------------------------------------------------------- /res/icons/f-crm.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jgaa/f-crm/HEAD/res/icons/f-crm.icns -------------------------------------------------------------------------------- /res/icons/f_crm.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jgaa/f-crm/HEAD/res/icons/f_crm.ico -------------------------------------------------------------------------------- /src/version.h: -------------------------------------------------------------------------------- 1 | #ifndef VERSION_H 2 | #define VERSION_H 3 | 4 | #define F_CRM_VERSION "0.02.00 BETA" 5 | 6 | #endif // VERSION_H 7 | -------------------------------------------------------------------------------- /src/utility.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILITY_H 2 | #define UTILITY_H 3 | 4 | #include 5 | #include 6 | 7 | time_t ToTime(const QDate& date); 8 | 9 | 10 | #endif // UTILITY_H 11 | -------------------------------------------------------------------------------- /res/icons/activity.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utility.cpp: -------------------------------------------------------------------------------- 1 | #include "src/utility.h" 2 | 3 | 4 | time_t ToTime(const QDate &date) 5 | { 6 | struct std::tm tm = {}; 7 | 8 | tm.tm_year = date.year() - 1900; 9 | tm.tm_mon = date.month() -1; 10 | tm.tm_mday = date.day(); 11 | 12 | return std::mktime(&tm); 13 | } 14 | -------------------------------------------------------------------------------- /res/icons/state_defined.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/action_waiting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/outgoing.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/person.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/incoming.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/move_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/move_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/status_watching.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/nofavourite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/status_prospect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/mail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/action_type_task.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/note.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/proposal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/research.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/external-link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/aboutdialog.h: -------------------------------------------------------------------------------- 1 | #ifndef ABOUTDIALOG_H 2 | #define ABOUTDIALOG_H 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class AboutDialog; 8 | } 9 | 10 | class AboutDialog : public QDialog 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit AboutDialog(QWidget *parent); 16 | ~AboutDialog(); 17 | 18 | private: 19 | Ui::AboutDialog *ui; 20 | }; 21 | 22 | #endif // ABOUTDIALOG_H 23 | -------------------------------------------------------------------------------- /res/icons/users.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/journalproxymodel.cpp: -------------------------------------------------------------------------------- 1 | #include "src/journalproxymodel.h" 2 | 3 | JournalProxyModel::JournalProxyModel(JournalModel *docModel, QObject *parent) 4 | : QSortFilterProxyModel(parent), model_{docModel} 5 | { 6 | setSourceModel(model_); 7 | } 8 | 9 | Qt::ItemFlags JournalProxyModel::flags(const QModelIndex &ix) const 10 | { 11 | return QSortFilterProxyModel::flags(ix) & ~Qt::EditRole; 12 | } 13 | -------------------------------------------------------------------------------- /res/icons/log_general.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/state_progress.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/intent.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/ch_type_web.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/intent_type_manual.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/ch_type_other.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/release.h: -------------------------------------------------------------------------------- 1 | #ifndef RELEASE_H 2 | #define RELEASE_H 3 | 4 | #include 5 | 6 | template 7 | class Release 8 | { 9 | public: 10 | Release(F f) : f_{std::move(f)} {} 11 | 12 | ~Release() { 13 | f_(); 14 | } 15 | 16 | private: 17 | F f_; 18 | }; 19 | 20 | template 21 | auto make_release(F f) { 22 | return Release(std::move(f)); 23 | } 24 | 25 | 26 | 27 | #endif // RELEASE_H 28 | -------------------------------------------------------------------------------- /res/icons/status_candidate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/document.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/phone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | /.qmake.cache 16 | /.qmake.stash 17 | *.pro.user 18 | *.pro.user.* 19 | *.qbs.user 20 | *.qbs.user.* 21 | *.moc 22 | moc_*.cpp 23 | moc_*.h 24 | qrc_*.cpp 25 | ui_*.h 26 | Makefile* 27 | *build-* 28 | 29 | # QtCreator 30 | 31 | *.autosave 32 | 33 | # QtCtreator Qml 34 | *.qmlproject.user 35 | *.qmlproject.user.* 36 | 37 | # QtCtreator CMake 38 | CMakeLists.txt.user* 39 | 40 | -------------------------------------------------------------------------------- /res/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/aboutdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "src/aboutdialog.h" 2 | #include "ui_aboutdialog.h" 3 | #include "version.h" 4 | 5 | AboutDialog::AboutDialog(QWidget *parent) : 6 | QDialog(parent), 7 | ui(new Ui::AboutDialog) 8 | { 9 | ui->setupUi(this); 10 | ui->version->setText(QString("Version ") + F_CRM_VERSION); 11 | ui->icon->setPixmap(QIcon(":res/icons/f-crm.svg").pixmap({ui->icon->width(), ui->icon->height()})); 12 | 13 | connect(ui->okButton, SIGNAL(clicked()), this, SLOT(close())); 14 | } 15 | 16 | AboutDialog::~AboutDialog() 17 | { 18 | delete ui; 19 | } 20 | -------------------------------------------------------------------------------- /src/journalproxymodel.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGPROXYMODEL_H 2 | #define LOGPROXYMODEL_H 3 | 4 | 5 | #include 6 | #include 7 | 8 | #include "journalmodel.h" 9 | 10 | class JournalProxyModel : public QSortFilterProxyModel 11 | { 12 | public: 13 | JournalProxyModel(JournalModel *docModel, QObject *parent = Q_NULLPTR); 14 | 15 | // QAbstractItemModel interface 16 | public: 17 | Qt::ItemFlags flags(const QModelIndex &index) const override; 18 | 19 | private: 20 | JournalModel *model_ = {}; 21 | 22 | }; 23 | 24 | #endif // LOGPROXYMODEL_H 25 | -------------------------------------------------------------------------------- /src/strategy.h: -------------------------------------------------------------------------------- 1 | #ifndef STRATEGY_H 2 | #define STRATEGY_H 3 | 4 | #include 5 | 6 | class Strategy { 7 | public: 8 | Strategy(QSqlTableModel& model, QSqlTableModel::EditStrategy newStrategy) 9 | : model_{model}, old_strategy_{model.editStrategy()} 10 | { 11 | model_.setEditStrategy(newStrategy); 12 | } 13 | 14 | ~Strategy() { 15 | model_.setEditStrategy(old_strategy_); 16 | } 17 | 18 | private: 19 | QSqlTableModel& model_; 20 | const QSqlTableModel::EditStrategy old_strategy_; 21 | }; 22 | 23 | #endif // STRATEGY_H 24 | -------------------------------------------------------------------------------- /res/icons/action_type_channel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/favoritesdialog.h: -------------------------------------------------------------------------------- 1 | #ifndef FAVORITESDIALOG_H 2 | #define FAVORITESDIALOG_H 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class FavoritesDialog; 8 | } 9 | 10 | class FavoritesDialog : public QDialog 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit FavoritesDialog(int row, int stars, QWidget *parent = 0); 16 | ~FavoritesDialog(); 17 | 18 | signals: 19 | void setStars(const int row, const int stars); 20 | 21 | private: 22 | Ui::FavoritesDialog *ui; 23 | const int row_; 24 | 25 | // QDialog interface 26 | public slots: 27 | void accept() override; 28 | }; 29 | 30 | #endif // FAVORITESDIALOG_H 31 | -------------------------------------------------------------------------------- /src/channel.h: -------------------------------------------------------------------------------- 1 | #ifndef CHANNEL_H 2 | #define CHANNEL_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | enum class ChannelType { 9 | OTHER, 10 | WEB, 11 | EMAIL, 12 | PHONE, 13 | MOBILE, 14 | SKYPE, 15 | LINKEDIN, 16 | REDDIT, 17 | FACEBOOK, 18 | GITHUB 19 | }; 20 | 21 | QIcon GetChannelStatusIcon(const ChannelType type); 22 | QIcon GetChannelStatusIcon(const int type); 23 | QString GetChannelTypeName(const ChannelType type); 24 | QString GetChannelTypeName(const int type); 25 | const std::array& GetChannelTypeEnums(); 26 | ChannelType ToChannelType(const int type); 27 | 28 | #endif // CHANNEL_H 29 | -------------------------------------------------------------------------------- /src/intentproxymodel.h: -------------------------------------------------------------------------------- 1 | #ifndef INTENTPROXYMODEL_H 2 | #define INTENTPROXYMODEL_H 3 | 4 | #include 5 | #include 6 | 7 | #include "intent.h" 8 | #include "intentsmodel.h" 9 | 10 | class IntentProxyModel : public QSortFilterProxyModel 11 | { 12 | public: 13 | IntentProxyModel(IntentsModel *docModel, QObject *parent = Q_NULLPTR); 14 | 15 | // QAbstractItemModel interface 16 | public: 17 | QVariant data(const QModelIndex &index, int role) const override; 18 | Qt::ItemFlags flags(const QModelIndex &index) const override; 19 | 20 | private: 21 | IntentsModel *model_ = {}; 22 | 23 | }; 24 | 25 | #endif // INTENTPROXYMODEL_H 26 | -------------------------------------------------------------------------------- /src/actionproxymodel.h: -------------------------------------------------------------------------------- 1 | #ifndef ACTIONPROXYMODEL_H 2 | #define ACTIONPROXYMODEL_H 3 | 4 | #include 5 | #include 6 | 7 | #include "action.h" 8 | #include "actionsmodel.h" 9 | 10 | class ActionProxyModel : public QSortFilterProxyModel 11 | { 12 | public: 13 | ActionProxyModel(ActionsModel *docModel, QObject *parent = Q_NULLPTR); 14 | 15 | // QAbstractItemModel interface 16 | public: 17 | QVariant data(const QModelIndex &index, int role) const override; 18 | Qt::ItemFlags flags(const QModelIndex &index) const override; 19 | 20 | private: 21 | ActionsModel *model_ = {}; 22 | 23 | }; 24 | 25 | 26 | #endif // ACTIONPROXYMODEL_H 27 | -------------------------------------------------------------------------------- /src/channelproxymodel.h: -------------------------------------------------------------------------------- 1 | #ifndef CHANNELPROXYMODEL_H 2 | #define CHANNELPROXYMODEL_H 3 | 4 | #include 5 | #include 6 | 7 | #include "channel.h" 8 | #include "channelsmodel.h" 9 | 10 | class ChannelProxyModel : public QSortFilterProxyModel 11 | { 12 | public: 13 | ChannelProxyModel(ChannelsModel *docModel, QObject *parent = Q_NULLPTR); 14 | 15 | // QAbstractItemModel interface 16 | public: 17 | QVariant data(const QModelIndex &index, int role) const override; 18 | Qt::ItemFlags flags(const QModelIndex &index) const override; 19 | 20 | private: 21 | ChannelsModel *model_ = {}; 22 | 23 | }; 24 | 25 | #endif // CHANNELPROXYMODEL_H 26 | -------------------------------------------------------------------------------- /src/documentproxymodel.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTPROXYMODEL_H 2 | #define DOCUMENTPROXYMODEL_H 3 | 4 | #include 5 | #include 6 | 7 | #include "document.h" 8 | #include "documentsmodel.h" 9 | 10 | class DocumentProxyModel : public QSortFilterProxyModel 11 | { 12 | public: 13 | DocumentProxyModel(DocumentsModel *docModel, QObject *parent = Q_NULLPTR); 14 | 15 | // QAbstractItemModel interface 16 | public: 17 | QVariant data(const QModelIndex &index, int role) const override; 18 | Qt::ItemFlags flags(const QModelIndex &index) const override; 19 | 20 | private: 21 | DocumentsModel *model_ = {}; 22 | 23 | }; 24 | 25 | #endif // DOCUMENTPROXYMODEL_H 26 | -------------------------------------------------------------------------------- /src/settingsdialog.h: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGSDIALOG_H 2 | #define SETTINGSDIALOG_H 3 | 4 | #include 5 | #include 6 | 7 | namespace Ui { 8 | class SettingsDialog; 9 | } 10 | 11 | class SettingsDialog : public QDialog 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit SettingsDialog(QSettings& settings, QWidget *parent = 0); 17 | ~SettingsDialog(); 18 | 19 | signals: 20 | void logSettingsChanged(); 21 | 22 | private: 23 | Ui::SettingsDialog *ui; 24 | QSettings& settings_; 25 | 26 | // QDialog interface 27 | public slots: 28 | void accept() override; 29 | 30 | private slots: 31 | void selectDbFile(); 32 | }; 33 | 34 | #endif // SETTINGSDIALOG_H 35 | -------------------------------------------------------------------------------- /src/actionexecutedialog.h: -------------------------------------------------------------------------------- 1 | #ifndef ACTIONEXECUTEDIALOG_H 2 | #define ACTIONEXECUTEDIALOG_H 3 | 4 | #include 5 | 6 | #include "channel.h" 7 | 8 | namespace Ui { 9 | class ActionExecuteDialog; 10 | } 11 | 12 | class ActionExecuteDialog : public QDialog 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | explicit ActionExecuteDialog(const QList& data, 18 | const int type, 19 | QWidget *parent = 0); 20 | ~ActionExecuteDialog(); 21 | 22 | QString value; 23 | 24 | private: 25 | Ui::ActionExecuteDialog *ui; 26 | 27 | // QDialog interface 28 | public slots: 29 | void accept() override; 30 | }; 31 | 32 | #endif // ACTIONEXECUTEDIALOG_H 33 | -------------------------------------------------------------------------------- /src/contactproxymodel.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTACTPROXYMODEL_H 2 | #define CONTACTPROXYMODEL_H 3 | 4 | #include "contact.h" 5 | #include "contactsmodel.h" 6 | 7 | #include 8 | #include 9 | 10 | #include "document.h" 11 | #include "documentsmodel.h" 12 | 13 | class ContactProxyModel : public QSortFilterProxyModel 14 | { 15 | public: 16 | ContactProxyModel(ContactsModel *docModel, QObject *parent = Q_NULLPTR); 17 | 18 | // QAbstractItemModel interface 19 | public: 20 | QVariant data(const QModelIndex &index, int role) const override; 21 | Qt::ItemFlags flags(const QModelIndex &index) const override; 22 | 23 | private: 24 | ContactsModel *model_ = {}; 25 | 26 | }; 27 | 28 | #endif // CONTACTPROXYMODEL_H 29 | -------------------------------------------------------------------------------- /src/favoritesdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "src/favoritesdialog.h" 2 | #include "ui_favoritesdialog.h" 3 | 4 | FavoritesDialog::FavoritesDialog(int row, int stars, QWidget *parent) : 5 | QDialog(parent), 6 | ui(new Ui::FavoritesDialog), row_{row} 7 | { 8 | ui->setupUi(this); 9 | 10 | for(auto i = 0; i <= 5; i++) { 11 | ui->stars->insertItem(i, 12 | new QListWidgetItem( 13 | QIcon(QStringLiteral(":/res/icons/%1star").arg(i)), 14 | "", ui->stars, 0)); 15 | } 16 | 17 | ui->stars->setCurrentRow(stars); 18 | } 19 | 20 | 21 | FavoritesDialog::~FavoritesDialog() 22 | { 23 | delete ui; 24 | } 25 | 26 | 27 | void FavoritesDialog::accept() 28 | { 29 | emit setStars(row_, ui->stars->currentRow()); 30 | QDialog::accept(); 31 | } 32 | -------------------------------------------------------------------------------- /ci/jenkins/Dockefile.debian-testing: -------------------------------------------------------------------------------- 1 | FROM debian:testing 2 | 3 | MAINTAINER Jarle Aase 4 | 5 | # In case you need proxy 6 | #RUN echo 'Acquire::http::Proxy "http://127.0.0.1:8080";' >> /etc/apt/apt.conf 7 | 8 | RUN apt-get -q update &&\ 9 | apt-get -y -q --no-install-recommends upgrade &&\ 10 | apt-get -y -q --no-install-recommends install openssh-server g++ git make \ 11 | qtdeclarative5-dev qt5-default ruby ruby-dev rubygems build-essential \ 12 | openjdk-8-jdk &&\ 13 | gem install --no-ri --no-rdoc fpm &&\ 14 | apt-get -y -q autoremove &&\ 15 | apt-get -y -q clean 16 | 17 | # Set user jenkins to the image 18 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 19 | echo "jenkins:jenkins" | chpasswd 20 | 21 | # Standard SSH port 22 | EXPOSE 22 23 | 24 | # Default command 25 | CMD ["/usr/sbin/sshd", "-D"] 26 | -------------------------------------------------------------------------------- /src/actionexecutedialog.cpp: -------------------------------------------------------------------------------- 1 | #include "src/actionexecutedialog.h" 2 | #include "ui_actionexecutedialog.h" 3 | 4 | ActionExecuteDialog::ActionExecuteDialog(const QList& data, 5 | const int type, 6 | QWidget *parent) : 7 | QDialog(parent), 8 | ui(new Ui::ActionExecuteDialog) 9 | { 10 | ui->setupUi(this); 11 | 12 | auto icon = GetChannelStatusIcon(type); 13 | 14 | for(const auto& v : data ) { 15 | ui->channels->addItem(new QListWidgetItem(icon, v, ui->channels)); 16 | } 17 | 18 | ui->channels->setCurrentRow(0); 19 | } 20 | 21 | 22 | ActionExecuteDialog::~ActionExecuteDialog() 23 | { 24 | delete ui; 25 | } 26 | 27 | 28 | void ActionExecuteDialog::accept() 29 | { 30 | value = ui->channels->currentItem()->text(); 31 | QDialog::accept(); 32 | } 33 | -------------------------------------------------------------------------------- /src/intentdialog.h: -------------------------------------------------------------------------------- 1 | #ifndef INTENTDIALOG_H 2 | #define INTENTDIALOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "src/intentsmodel.h" 9 | 10 | namespace Ui { 11 | class IntentDialog; 12 | } 13 | 14 | class IntentDialog : public QDialog 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit IntentDialog(QWidget *parent = 0); 20 | ~IntentDialog(); 21 | 22 | void setRecord(const QSqlRecord& rec); 23 | void setModel(IntentsModel *model, QModelIndex& ix); 24 | 25 | signals: 26 | void addIntent(const QSqlRecord& rec); 27 | 28 | private: 29 | Ui::IntentDialog *ui; 30 | QSqlRecord rec_; 31 | QDataWidgetMapper *mapper_ = {}; 32 | 33 | 34 | // QDialog interface 35 | public slots: 36 | void accept() override; 37 | void reject() override; 38 | }; 39 | 40 | #endif // INTENTDIALOG_H 41 | -------------------------------------------------------------------------------- /src/channeldialog.h: -------------------------------------------------------------------------------- 1 | #ifndef CHANNELDIALOG_H 2 | #define CHANNELDIALOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "src/channelsmodel.h" 9 | 10 | namespace Ui { 11 | class ChannelDialog; 12 | } 13 | 14 | class ChannelDialog : public QDialog 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit ChannelDialog(QWidget *parent = 0); 20 | ~ChannelDialog(); 21 | 22 | void setRecord(const QSqlRecord& rec); 23 | void setModel(ChannelsModel *model, QModelIndex& ix); 24 | 25 | signals: 26 | void addChannel(const QSqlRecord& rec); 27 | 28 | private: 29 | Ui::ChannelDialog *ui; 30 | QSqlRecord rec_; 31 | QDataWidgetMapper *mapper_ = {}; 32 | 33 | // QDialog interface 34 | public slots: 35 | void accept() override; 36 | void reject() override; 37 | }; 38 | 39 | #endif // CHANNELDIALOG_H 40 | -------------------------------------------------------------------------------- /src/persondialog.h: -------------------------------------------------------------------------------- 1 | #ifndef PERSONDIALOG_H 2 | #define PERSONDIALOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "src/contactsmodel.h" 9 | 10 | 11 | namespace Ui { 12 | class PersonDialog; 13 | } 14 | 15 | class PersonDialog : public QDialog 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | explicit PersonDialog(ContactsModel& model, bool isPerson, int row, QWidget *parent = 0); 21 | ~PersonDialog(); 22 | 23 | signals: 24 | void addPerson(const QSqlRecord& rec); 25 | void updatePerson(const int row, const QSqlRecord& rec); 26 | void setFilter(QString value); 27 | 28 | private: 29 | Ui::PersonDialog *ui; 30 | const bool is_person_; 31 | const int row_; 32 | ContactsModel& model_; 33 | QDataWidgetMapper mapper_; 34 | 35 | // QDialog interface 36 | public slots: 37 | void accept() override; 38 | }; 39 | 40 | #endif // PERSONDIALOG_H 41 | -------------------------------------------------------------------------------- /src/actiondialog.h: -------------------------------------------------------------------------------- 1 | #ifndef ACTIONDIALOG_H 2 | #define ACTIONDIALOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "actionsmodel.h" 9 | 10 | namespace Ui { 11 | class ActionDialog; 12 | } 13 | 14 | class ActionDialog : public QDialog 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit ActionDialog(const int contact, QWidget *parent = 0); 20 | ~ActionDialog(); 21 | 22 | void setRecord(const QSqlRecord& rec); 23 | void setModel(ActionsModel *model, QModelIndex& ix); 24 | 25 | signals: 26 | void addAction(const QSqlRecord& rec); 27 | 28 | private: 29 | void checkAccess(); 30 | 31 | Ui::ActionDialog *ui; 32 | QSqlRecord rec_; 33 | QDataWidgetMapper *mapper_ = {}; 34 | 35 | // QDialog interface 36 | public slots: 37 | void accept() override; 38 | void reject() override; 39 | 40 | private slots: 41 | void onCurrentIndexChanged(int index); 42 | 43 | }; 44 | 45 | #endif // ACTIONDIALOG_H 46 | -------------------------------------------------------------------------------- /src/intent.h: -------------------------------------------------------------------------------- 1 | #ifndef INTENTS_H 2 | #define INTENTS_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | enum class IntentType { 10 | MANUAL 11 | }; 12 | 13 | enum class IntentState { 14 | DEFINED, 15 | PROGRESS, 16 | SUCCEEDED, 17 | FAILED, 18 | TERMINATED 19 | }; 20 | 21 | 22 | QIcon GetIntentTypeIcon(const IntentType type); 23 | QIcon GetIntentTypeIcon(const int type); 24 | IntentType ToIntentType(const int type); 25 | const QString& GetIntentTypeName(const IntentType type); 26 | const QString& GetIntentTypeName(const int type); 27 | const std::array& GetIntentTypeEnums(); 28 | 29 | 30 | QIcon GetIntentStateIcon(const IntentState type); 31 | QIcon GetIntentStateIcon(const int type); 32 | IntentState ToIntentState(const int type); 33 | const QString& GetIntentStateName(const IntentState type); 34 | const QString& GetIntentStateName(const int type); 35 | const std::array& GetIntentStateEnums(); 36 | 37 | 38 | #endif // INTENTS_H 39 | -------------------------------------------------------------------------------- /src/action.h: -------------------------------------------------------------------------------- 1 | #ifndef ACTION_H 2 | #define ACTION_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | enum class ActionState { 10 | WAITING, 11 | OPEN, 12 | BLOCKED, 13 | ON_HOLD, 14 | DONE, 15 | CANCELLED, 16 | FAILED 17 | }; 18 | 19 | enum class ActionType { 20 | TASK, 21 | CHANNEL, 22 | MEETING 23 | }; 24 | 25 | QIcon GetActionStateIcon(const ActionState type); 26 | QIcon GetActionStateIcon(const int type); 27 | ActionState ToActionState(const int type); 28 | const QString& GetActionStateName(const ActionState type); 29 | const QString& GetActionStateName(const int type); 30 | const std::array& GetActionStateEnums(); 31 | 32 | 33 | QIcon GetActionTypeIcon(const ActionType type); 34 | QIcon GetActionTypeIcon(const int type); 35 | ActionType ToActionType(const int type); 36 | const QString& GetActionTypeName(const ActionType type); 37 | const QString& GetActionTypeName(const int type); 38 | const std::array& GetActionTypeEnums(); 39 | 40 | #endif // ACTION_H 41 | -------------------------------------------------------------------------------- /src/logging.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGGING_H 2 | #define LOGGING_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | class Logging : public QObject 10 | { 11 | Q_OBJECT 12 | 13 | public: 14 | Logging(); 15 | ~Logging(); 16 | 17 | static void logMessageHandler(QtMsgType type, 18 | const QMessageLogContext &context, 19 | const QString &msg); 20 | 21 | void onLogMessageHandler(QtMsgType type, 22 | const QMessageLogContext &context, 23 | const QString &msg); 24 | static Logging *instance() { 25 | return instance_; 26 | } 27 | 28 | public slots: 29 | // Re-open the log-file, applying the current settings 30 | void changed(); 31 | 32 | signals: 33 | void message(const QString& label, const QString& text); 34 | 35 | private: 36 | void open(); 37 | 38 | std::unique_ptr logFile_; 39 | static Logging *instance_; 40 | QSettings settings_; 41 | }; 42 | 43 | #endif // LOGGING_H 44 | -------------------------------------------------------------------------------- /src/contactproxymodel.cpp: -------------------------------------------------------------------------------- 1 | #include "src/contactproxymodel.h" 2 | #include 3 | 4 | ContactProxyModel::ContactProxyModel(ContactsModel *docModel, QObject *parent) 5 | : QSortFilterProxyModel(parent), model_{docModel} 6 | { 7 | setSourceModel(model_); 8 | } 9 | 10 | QVariant ContactProxyModel::data(const QModelIndex &ix, int role) const 11 | { 12 | static const int h_status = model_->fieldIndex("status"); 13 | 14 | if (role == Qt::DisplayRole || role == Qt::EditRole) { 15 | 16 | if (ix.column() == h_status) { 17 | return GetContactStatusName(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 18 | } 19 | } 20 | 21 | return model_->data(ix, role); 22 | } 23 | 24 | 25 | Qt::ItemFlags ContactProxyModel::flags(const QModelIndex &ix) const 26 | { 27 | static const int h_name = model_->fieldIndex("name"); 28 | 29 | if (ix.isValid()) { 30 | if ((ix.column() == h_name)) { 31 | return QSortFilterProxyModel::flags(ix); 32 | } 33 | } 34 | 35 | 36 | return QSortFilterProxyModel::flags(ix) & ~Qt::EditRole; 37 | } 38 | -------------------------------------------------------------------------------- /src/database.h: -------------------------------------------------------------------------------- 1 | #ifndef DATABASE_H 2 | #define DATABASE_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | class Database : public QObject 17 | { 18 | Q_OBJECT 19 | public: 20 | enum FcrmTable { 21 | FCRM_VERSION = 0 22 | }; 23 | 24 | 25 | struct Error : public std::runtime_error 26 | { 27 | explicit Error(const char *what) : std::runtime_error(what) {} 28 | explicit Error(const QString& what) : std::runtime_error(what.toStdString()) {} 29 | }; 30 | 31 | Database(QObject *parent); 32 | ~Database(); 33 | 34 | enum DsTable { 35 | DS_VERSION = 0 36 | }; 37 | 38 | QSqlDatabase& getDb() { return db_; } 39 | 40 | signals: 41 | 42 | public slots: 43 | 44 | protected: 45 | void createDatabase(); 46 | void exec(const char *sql); 47 | 48 | static constexpr int currentVersion = 1; 49 | QSqlDatabase db_; 50 | }; 51 | 52 | 53 | #endif // DATABASE_H 54 | -------------------------------------------------------------------------------- /src/upcomingmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef UPCOMINGMODEL_H 2 | #define UPCOMINGMODEL_H 3 | 4 | #include 5 | #include 6 | 7 | 8 | class UpcomingModel : public QSqlQueryModel 9 | { 10 | Q_OBJECT 11 | public: 12 | enum class Mode { 13 | CONTACT_UPCOMING, 14 | TODAY, 15 | UPCOMING 16 | }; 17 | 18 | enum Headers { 19 | H_ID, 20 | H_STATE, 21 | H_START_DATE, 22 | H_CONTACT_ID, 23 | H_CONTACT_NAME, 24 | H_CONTACT_STATUS, 25 | H_INTENT_ID, 26 | H_INTENT_ABSTRACT, 27 | H_PERSON_ID, 28 | H_PERSON_NAME, 29 | H_NAME, 30 | H_DUE_DATE, 31 | H_DESIRED_OUTCOME 32 | }; 33 | 34 | constexpr static int NO_SELECTION = -1; 35 | 36 | UpcomingModel(QSettings& settings, QObject *parent, Mode mode); 37 | 38 | QVariant data(const QModelIndex &index, int role) const override; 39 | 40 | 41 | public slots: 42 | void setContact(int contact = NO_SELECTION); 43 | void select(); 44 | 45 | private: 46 | QSqlQuery createQuery() const; 47 | 48 | const Mode mode_; 49 | QSettings& settings_; 50 | int contact_ = NO_SELECTION; 51 | }; 52 | 53 | #endif // UPCOMINGMODEL_H 54 | -------------------------------------------------------------------------------- /scripts/package-windows.bat: -------------------------------------------------------------------------------- 1 | 2 | rem On my machine, I execute the build script from a script with these commands: 3 | rem 4 | rem SET QTDIR=C:\Qt\5.10.0\msvc2017_64 5 | rem call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\amd64\vcvars64.bat" 6 | rem PATH=%PATH%;C:\Program Files (x86)\Windows Kits\10\bin\10.0.16299.0\x64 7 | rem cd "C:\Users\Jarle Aase\src\f-crm\scripts" 8 | rem call .\package-windows 9 | 10 | echo on 11 | 12 | IF NOT DEFINED DIST_DIR (set DIST_DIR=%cd%\dist\windows) 13 | IF NOT DEFINED BUILD_DIR (set BUILD_DIR=%DIST_DIR%\build) 14 | IF NOT DEFINED SRC_DIR (set SRC_DIR=%cd%\..) 15 | IF NOT DEFINED OUT_DIR (set OUT_DIR=%DIST_DIR%\f-crm) 16 | 17 | rmdir /S /Q "%DIST_DIR%" 18 | mkdir "%DIST_DIR%" 19 | mkdir "%BUILD_DIR%" 20 | mkdir "%OUT_DIR%" 21 | 22 | pushd "%BUILD_DIR%" 23 | 24 | %QTDIR%\bin\qmake.exe ^ 25 | -spec win32-msvc ^ 26 | "CONFIG += release" ^ 27 | "%SRC_DIR%\f-crm.pro" 28 | 29 | nmake 30 | 31 | popd 32 | 33 | echo "Copying: %BUILD_DIR%\release\f_crm.exe" "%OUT_DIR%" 34 | copy "%BUILD_DIR%\release\f-crm.exe" "%OUT_DIR%" 35 | copy "%SRC_DIR%\res\icons\f_crm.ico" "%OUT_DIR%" 36 | 37 | %QTDIR%\bin\windeployqt "%OUT_DIR%\f-crm.exe" 38 | 39 | echo "The prepared package is in: "%OUT_DIR%" 40 | -------------------------------------------------------------------------------- /src/tableviewwithdrop.h: -------------------------------------------------------------------------------- 1 | #ifndef TABLEVIEWWITHDROP_H 2 | #define TABLEVIEWWITHDROP_H 3 | 4 | #include "document.h" 5 | #include "documentsmodel.h" 6 | #include "documentdialog.h" 7 | #include 8 | 9 | 10 | class TableViewWithDrop : public QTableView 11 | { 12 | public: 13 | TableViewWithDrop(QWidget *parent = Q_NULLPTR); 14 | 15 | void setDocumentDropEnabled(bool enable) { enabled_ = enable; } 16 | void setDocumentsModel(DocumentsModel *model); 17 | void setContactId(const int id); 18 | void setEntity(Document::Entity entity, QSqlTableModel *model, const int id); 19 | bool canDoDrop() const; 20 | 21 | // QWidget interface 22 | protected: 23 | void dragEnterEvent(QDragEnterEvent *event) override; 24 | void dragMoveEvent(QDragMoveEvent *event) override; 25 | void dragLeaveEvent(QDragLeaveEvent *event) override; 26 | void dropEvent(QDropEvent *event) override; 27 | 28 | private: 29 | void addDocument(const int row, const QUrl& url); 30 | 31 | bool enabled_ = false; 32 | DocumentsModel *document_model_ = {}; 33 | QSqlTableModel *entity_model_ = {}; 34 | int contact_id_ = {}; 35 | int entity_id_ = {}; 36 | Document::Entity entity_ = Document::Entity::CONTACT; 37 | }; 38 | 39 | #endif // TABLEVIEWWITHDROP_H 40 | -------------------------------------------------------------------------------- /ci/jenkins/Dockefile.debian-stretch: -------------------------------------------------------------------------------- 1 | # Based on: https://github.com/evarga/docker-images/blob/master/jenkins-slave/Dockerfile 2 | FROM debian:stretch 3 | 4 | MAINTAINER Jarle Aase 5 | 6 | # In case you need proxy 7 | #RUN echo 'Acquire::http::Proxy "http://127.0.0.1:8080";' >> /etc/apt/apt.conf 8 | 9 | RUN apt-get -q update &&\ 10 | DEBIAN_FRONTEND="noninteractive" apt-get -q upgrade -y -o Dpkg::Options::="--force-confnew" --no-install-recommends &&\ 11 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends openssh-server &&\ 12 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y g++ git make \ 13 | qtdeclarative5-dev qt5-default ruby ruby-dev rubygems build-essential \ 14 | openjdk-8-jdk &&\ 15 | gem install --no-ri --no-rdoc fpm &&\ 16 | apt-get -q autoremove &&\ 17 | apt-get -q clean -y && rm -rf /var/lib/apt/lists/* && rm -f /var/cache/apt/*.bin &&\ 18 | sed -i 's|session required pam_loginuid.so|session optional pam_loginuid.so|g' /etc/pam.d/sshd &&\ 19 | mkdir -p /var/run/sshd 20 | 21 | 22 | # Set user jenkins to the image 23 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 24 | echo "jenkins:jenkins" | chpasswd 25 | 26 | # Standard SSH port 27 | EXPOSE 22 28 | 29 | # Default command 30 | CMD ["/usr/sbin/sshd", "-D"] 31 | -------------------------------------------------------------------------------- /src/contact.h: -------------------------------------------------------------------------------- 1 | #ifndef PERSON_H 2 | #define PERSON_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | enum class ContactType { 10 | CORPORATION, 11 | INDIVID 12 | }; 13 | 14 | enum class ContactGender { 15 | UNKNOWN, 16 | MALE, 17 | FEMALE 18 | }; 19 | 20 | enum class ContactStatus { 21 | CANDIDATE, 22 | WATCHING, 23 | PROSPECT, 24 | CUSTOMER, 25 | LOST, 26 | BANNED, 27 | SHUT_DOWN // No longer in business 28 | }; 29 | 30 | QIcon GetContactTypeIcon(const ContactType type); 31 | QIcon GetContactTypeIcon(const int type); 32 | ContactType ToContactType(const int type); 33 | 34 | QIcon GetContactGenderIcon(const ContactGender type); 35 | QIcon GetContactGenderIcon(const int type); 36 | ContactGender ToContactGender(const int type); 37 | const QString& GetContactGenderName(const ContactGender type); 38 | const QString& GetContactGenderName(const int type); 39 | const std::array& GetContactGenderEnums(); 40 | 41 | QIcon GetContactStatusIcon(const ContactStatus type); 42 | QIcon GetContactStatusIcon(const int type); 43 | ContactStatus ToContactStatus(const int type); 44 | const QString& GetContactStatusName(const ContactStatus type); 45 | const QString& GetContactStatusName(const int type); 46 | const std::array& GetContactStatusEnums(); 47 | 48 | 49 | #endif // PERSON_H 50 | -------------------------------------------------------------------------------- /scripts/package-deb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Compile and prepare a .deb package for distribution 4 | # Uses the QT libraries from the lilnux ditribution 5 | # 6 | # Example: 7 | # F_CRM_VERSION="2.0.1" DIST_DIR=`pwd`/dist BUILD_DIR=`pwd`/build SRC_DIR=`pwd`/f-crm ./f-crm/scripts/package-deb.sh 8 | 9 | if [ -z "$F_CRM_VERSION" ]; then 10 | F_CRM_VERSION="0.0.1" 11 | echo "Warning: Missing F_CRM_VERSION variable!" 12 | fi 13 | 14 | if [ -z ${DIST_DIR:-} ]; then 15 | DIST_DIR=`pwd`/dist/linux 16 | fi 17 | 18 | if [ -z ${BUILD_DIR:-} ]; then 19 | BUILD_DIR=`pwd`/build 20 | fi 21 | 22 | if [ -z ${SRC_DIR:-} ]; then 23 | # Just assume we are run from the scipts directory 24 | SRC_DIR=`pwd`/.. 25 | fi 26 | 27 | echo "Building f-crm for linux into ${DIST_DIR} from ${SRC_DIR}" 28 | 29 | rm -rf $DIST_DIR $BUILD_DIR 30 | 31 | mkdir -p $DIST_DIR &&\ 32 | pushd $DIST_DIR &&\ 33 | mkdir -p $BUILD_DIR &&\ 34 | pushd $BUILD_DIR &&\ 35 | qmake $SRC_DIR/f-crm.pro &&\ 36 | make && make install &&\ 37 | popd &&\ 38 | fpm --input-type dir \ 39 | --output-type deb \ 40 | --force \ 41 | --name f-crm \ 42 | --version ${F_CRM_VERSION} \ 43 | --vendor "The Last Viking LTD" \ 44 | --description "Time Tracking for Freelancers and Independenet Contractors" \ 45 | --depends qt5-default --depends libsqlite3-0 \ 46 | --chdir ${DIST_DIR}/root/ \ 47 | --package ${DIST_NAME}f-crm-VERSION_ARCH.deb &&\ 48 | echo "Debian package is available in $PWD" &&\ 49 | popd 50 | -------------------------------------------------------------------------------- /res/icons/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/channelsmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef CHANNELSMODEL_H 2 | #define CHANNELSMODEL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "database.h" 11 | 12 | // Create read-only properties like 'name_col' for the database columns 13 | #define DEF_COLUMN(name) Q_PROPERTY(int name ## _col MEMBER h_ ## name ## _) 14 | 15 | 16 | class ChannelsModel : public QSqlTableModel 17 | { 18 | Q_OBJECT 19 | public: 20 | ChannelsModel(QSettings& settings, QObject *parent, QSqlDatabase db); 21 | 22 | DEF_COLUMN(id) 23 | DEF_COLUMN(contact) 24 | DEF_COLUMN(type) 25 | DEF_COLUMN(value) 26 | DEF_COLUMN(verified) 27 | DEF_COLUMN(name) 28 | 29 | void setContact(int id); 30 | 31 | public slots: 32 | void removeChannels(const QModelIndexList& indexes); 33 | void verifyChannels(const QModelIndexList& indexes, bool verified = true); 34 | void addChannel(const QSqlRecord& rec); 35 | 36 | private: 37 | QSettings& settings_; 38 | 39 | int h_id_ = {}; 40 | int h_contact_ = {}; 41 | int h_type_ = {}; 42 | int h_value_ = {}; 43 | int h_verified_ = {}; 44 | int h_name_ = {}; 45 | 46 | // QAbstractItemModel interface 47 | public: 48 | QVariant data(const QModelIndex &index, int role) const override; 49 | QVariant headerData(int section, Qt::Orientation orientation, int role) const override; 50 | }; 51 | 52 | 53 | #undef DEF_COLUMN 54 | 55 | #endif // CHANNELSMODEL_H 56 | -------------------------------------------------------------------------------- /src/documentdialog.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTDIALOG_H 2 | #define DOCUMENTDIALOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "document.h" 10 | #include "documentsmodel.h" 11 | 12 | namespace Ui { 13 | class DocumentDialog; 14 | } 15 | 16 | class DocumentDialog : public QDialog 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | explicit DocumentDialog(const QSqlRecord& rec, int row = 0, QWidget *parent = 0); 22 | ~DocumentDialog(); 23 | 24 | signals: 25 | void addDocument(const QSqlRecord& rec); 26 | signals: 27 | void updateDocument(const int row, const QSqlRecord& rec); 28 | 29 | private slots: 30 | void onEntityCurrentIndexChanged(int index); 31 | void onTypeCurrentIndexChanged(int index); 32 | void onLocationBtnClicked(bool checked); 33 | void onOpenBtnClicked(bool checked); 34 | 35 | private: 36 | void syncEntity(); 37 | void syncEntityToRec(); 38 | void syncType(); 39 | 40 | Ui::DocumentDialog *ui; 41 | QSqlRecord rec_; 42 | const int row_; 43 | 44 | // QDialog interface 45 | void fecthPersons(); 46 | QVariant getValue(const char *colName) const; 47 | void fetchContacts(int contactId, QComboBox *combo); 48 | void fetchPersons(int contactId, QComboBox *combo); 49 | void fetchIntents(int contactId, QComboBox *combo); 50 | void fetchActions(int contactId, QComboBox *combo); 51 | public slots: 52 | void accept() override; 53 | void reject() override; 54 | }; 55 | 56 | #endif // DOCUMENTDIALOG_H 57 | -------------------------------------------------------------------------------- /src/intentsmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef INTENTSMODEL_H 2 | #define INTENTSMODEL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "database.h" 12 | 13 | // Create read-only properties like 'name_col' for the database columns 14 | #define DEF_COLUMN(name) Q_PROPERTY(int name ## _col MEMBER h_ ## name ## _) 15 | 16 | 17 | class IntentsModel : public QSqlTableModel 18 | { 19 | Q_OBJECT 20 | public: 21 | IntentsModel(QSettings& settings, QObject *parent, QSqlDatabase db); 22 | 23 | DEF_COLUMN(id) 24 | DEF_COLUMN(contact) 25 | DEF_COLUMN(type) 26 | DEF_COLUMN(state) 27 | DEF_COLUMN(abstract) 28 | DEF_COLUMN(notes) 29 | DEF_COLUMN(created_date) 30 | 31 | void setContact(int id); 32 | int getIntentId(const QModelIndex& ix); 33 | 34 | public slots: 35 | void removeIntents(const QModelIndexList& indexes); 36 | void addIntent(QSqlRecord rec); 37 | void updateState(); 38 | 39 | private: 40 | QSettings& settings_; 41 | 42 | int h_id_ = {}; 43 | int h_contact_ = {}; 44 | int h_type_ = {}; 45 | int h_state_ = {}; 46 | int h_abstract_ = {}; 47 | int h_notes_ = {}; 48 | int h_created_date_ = {}; 49 | 50 | // QAbstractItemModel interface 51 | public: 52 | QVariant data(const QModelIndex &index, int role) const override; 53 | QVariant headerData(int section, Qt::Orientation orientation, int role) const override; 54 | }; 55 | 56 | 57 | #undef DEF_COLUMN 58 | 59 | #endif // INTENTSMODEL_H 60 | -------------------------------------------------------------------------------- /src/channelproxymodel.cpp: -------------------------------------------------------------------------------- 1 | #include "src/channelproxymodel.h" 2 | 3 | ChannelProxyModel::ChannelProxyModel(ChannelsModel *docModel, QObject *parent) 4 | : QSortFilterProxyModel(parent), model_{docModel} 5 | { 6 | setSourceModel(model_); 7 | } 8 | 9 | QVariant ChannelProxyModel::data(const QModelIndex &ix, int role) const 10 | { 11 | static const int h_value = model_->fieldIndex("value"); 12 | static const int h_verified = model_->fieldIndex("verified"); 13 | static const int h_type = model_->fieldIndex("type"); 14 | 15 | static const QIcon check_icon{":/res/icons/check.svg"}; 16 | 17 | if (ix.isValid()) { 18 | if (role == Qt::DecorationRole && ix.column() == h_value) { 19 | auto nix = index(ix.row(), h_type, {}); 20 | int status = model_->data(nix, Qt::DisplayRole).toInt(); 21 | return GetChannelStatusIcon(status); 22 | } 23 | 24 | if (ix.column() == h_verified) { 25 | if (role == Qt::DecorationRole) { 26 | auto nix = index(ix.row(), h_verified, {}); 27 | if (model_->data(nix, Qt::DisplayRole).toBool()) { 28 | return check_icon; 29 | } 30 | } 31 | 32 | if (role == Qt::DisplayRole) { 33 | return {}; 34 | } 35 | } 36 | } 37 | 38 | return model_->data(ix, role); 39 | } 40 | 41 | 42 | Qt::ItemFlags ChannelProxyModel::flags(const QModelIndex &ix) const 43 | { 44 | static const int h_value = model_->fieldIndex("value"); 45 | 46 | if (ix.isValid()) { 47 | if ((ix.column() == h_value)) { 48 | return QSortFilterProxyModel::flags(ix); 49 | } 50 | } 51 | 52 | return QSortFilterProxyModel::flags(ix) & ~Qt::EditRole; 53 | } 54 | -------------------------------------------------------------------------------- /ci/jenkins/Dockefile.ubuntu-bionic: -------------------------------------------------------------------------------- 1 | # Based on: https://github.com/evarga/docker-images/blob/master/jenkins-slave/Dockerfile 2 | FROM ubuntu:bionic 3 | 4 | MAINTAINER Jarle Aase 5 | 6 | # In case you need proxy 7 | #RUN echo 'Acquire::http::Proxy "http://127.0.0.1:8080";' >> /etc/apt/apt.conf 8 | 9 | RUN apt-get -q update &&\ 10 | DEBIAN_FRONTEND="noninteractive" apt-get -q upgrade -y -o Dpkg::Options::="--force-confnew" --no-install-recommends &&\ 11 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends openssh-server &&\ 12 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y g++ git make \ 13 | qtdeclarative5-dev qt5-default ruby ruby-dev rubygems build-essential &&\ 14 | gem install --no-ri --no-rdoc fpm &&\ 15 | apt-get -q autoremove &&\ 16 | apt-get -q clean -y && rm -rf /var/lib/apt/lists/* && rm -f /var/cache/apt/*.bin &&\ 17 | sed -i 's|session required pam_loginuid.so|session optional pam_loginuid.so|g' /etc/pam.d/sshd &&\ 18 | mkdir -p /var/run/sshd 19 | 20 | # Install JDK 8 (latest edition) 21 | RUN apt-get -q update &&\ 22 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends software-properties-common &&\ 23 | add-apt-repository -y ppa:openjdk-r/ppa &&\ 24 | apt-get -q update &&\ 25 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends openjdk-8-jre-headless &&\ 26 | apt-get -q clean -y && rm -rf /var/lib/apt/lists/* && rm -f /var/cache/apt/*.bin 27 | 28 | # Set user jenkins to the image 29 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 30 | echo "jenkins:jenkins" | chpasswd 31 | 32 | # Standard SSH port 33 | EXPOSE 22 34 | 35 | # Default command 36 | CMD ["/usr/sbin/sshd", "-D"] 37 | -------------------------------------------------------------------------------- /ci/jenkins/Dockefile.ubuntu-xenial: -------------------------------------------------------------------------------- 1 | # Based on: https://github.com/evarga/docker-images/blob/master/jenkins-slave/Dockerfile 2 | FROM ubuntu:xenial 3 | 4 | MAINTAINER Jarle Aase 5 | 6 | # In case you need proxy 7 | #RUN echo 'Acquire::http::Proxy "http://127.0.0.1:8080";' >> /etc/apt/apt.conf 8 | 9 | RUN apt-get -q update &&\ 10 | DEBIAN_FRONTEND="noninteractive" apt-get -q upgrade -y -o Dpkg::Options::="--force-confnew" --no-install-recommends &&\ 11 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends openssh-server &&\ 12 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y g++ git make \ 13 | qtdeclarative5-dev qt5-default ruby ruby-dev rubygems build-essential &&\ 14 | gem install --no-ri --no-rdoc fpm &&\ 15 | apt-get -q autoremove &&\ 16 | apt-get -q clean -y && rm -rf /var/lib/apt/lists/* && rm -f /var/cache/apt/*.bin &&\ 17 | sed -i 's|session required pam_loginuid.so|session optional pam_loginuid.so|g' /etc/pam.d/sshd &&\ 18 | mkdir -p /var/run/sshd 19 | 20 | # Install JDK 8 (latest edition) 21 | RUN apt-get -q update &&\ 22 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends software-properties-common &&\ 23 | add-apt-repository -y ppa:openjdk-r/ppa &&\ 24 | apt-get -q update &&\ 25 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends openjdk-8-jre-headless &&\ 26 | apt-get -q clean -y && rm -rf /var/lib/apt/lists/* && rm -f /var/cache/apt/*.bin 27 | 28 | # Set user jenkins to the image 29 | RUN useradd -m -d /home/jenkins -s /bin/sh jenkins &&\ 30 | echo "jenkins:jenkins" | chpasswd 31 | 32 | # Standard SSH port 33 | EXPOSE 22 34 | 35 | # Default command 36 | CMD ["/usr/sbin/sshd", "-D"] 37 | -------------------------------------------------------------------------------- /scripts/package-macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Compile and prepare a signed .dmg package for distribution 4 | # Assimes the environment-variable QTDIR to point to the 5 | # Qt installation 6 | # 7 | # Example: 8 | # Jarles-Mac-mini:scripts jgaa$ QTDIR=/Users/jgaa/Qt/5.10.0/clang_64 ./package-macos.sh 9 | 10 | APP="f-crm" 11 | 12 | if [ -z "$F_CRM_VERSION" ]; then 13 | F_CRM_VERSION="2.0.0" 14 | echo "Warning: Missing F_CRM_VERSION variable!" 15 | fi 16 | 17 | if [ -z ${DIST_DIR:-} ]; 18 | then 19 | DIST_DIR=`pwd`/dist/macos 20 | fi 21 | 22 | if [ -z ${SIGN_CERT:-} ]; 23 | then 24 | SIGN_CERT="Developer ID Application" 25 | fi 26 | 27 | if [ -z ${BUILD_DIR:-} ]; then 28 | BUILD_DIR=`pwd`/build 29 | fi 30 | 31 | if [ -z ${SRC_DIR:-} ]; 32 | then 33 | # Just assume we are run from the scipts directory 34 | SRC_DIR=`pwd`/.. 35 | fi 36 | 37 | echo "Building ${APP} for macos into ${DIST_DIR} from ${SRC_DIR}" 38 | 39 | rm -rf $DIST_DIR $BUILD_DIR 40 | 41 | mkdir -p $DIST_DIR && cd $DIST_DIR 42 | mkdir -p $BUILD_DIR 43 | 44 | pushd $BUILD_DIR 45 | 46 | $QTDIR/bin/qmake \ 47 | -spec macx-clang \ 48 | "CONFIG += release x86_64" \ 49 | $SRC_DIR/${APP}.pro 50 | 51 | make -j8 52 | 53 | popd 54 | 55 | pushd $DIST_DIR 56 | 57 | mv $BUILD_DIR/${APP}.app $BUILD_DIR/${APP}-${F_CRM_VERSION}.app 58 | 59 | echo "Making dmg package with $QTDIR/bin/macdeployqt" 60 | #$QTDIR/bin/macdeployqt $BUILD_DIR/${APP}-${F_CRM_VERSION}.app -dmg -appstore-compliant -codesign="$SIGN_CERT" 61 | 62 | $QTDIR/bin/macdeployqt $BUILD_DIR/${APP}-${F_CRM_VERSION}.app -dmg -appstore-compliant 63 | 64 | rm $BUILD_DIR/${APP}-${F_CRM_VERSION}.app/Contents/PlugIns/sqldrivers/libqsqlmysql.dylib 65 | 66 | $QTDIR/bin/macdeployqt $BUILD_DIR/${APP}-${F_CRM_VERSION}.app -dmg -always-overwrite codesign="$SIGN_CERT" 67 | 68 | 69 | mv $BUILD_DIR/${APP}-${F_CRM_VERSION}.dmg . 70 | 71 | popd 72 | -------------------------------------------------------------------------------- /res/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 60 | 61 | -------------------------------------------------------------------------------- /res/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 60 | 61 | -------------------------------------------------------------------------------- /res/icons/action_ready.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 62 | 63 | -------------------------------------------------------------------------------- /ui/favoritesdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | FavoritesDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 367 10 | 221 11 | 12 | 13 | 14 | Rate 15 | 16 | 17 | 18 | 19 | 20 | 21 | 160 22 | 32 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Qt::Horizontal 31 | 32 | 33 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | buttonBox 43 | accepted() 44 | FavoritesDialog 45 | accept() 46 | 47 | 48 | 248 49 | 254 50 | 51 | 52 | 157 53 | 274 54 | 55 | 56 | 57 | 58 | buttonBox 59 | rejected() 60 | FavoritesDialog 61 | reject() 62 | 63 | 64 | 316 65 | 260 66 | 67 | 68 | 286 69 | 274 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /res/icons/favourite.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 60 | 61 | -------------------------------------------------------------------------------- /res/icons/not_favourite.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 60 | 61 | -------------------------------------------------------------------------------- /res/icons/status_customer.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 60 | 63 | 64 | -------------------------------------------------------------------------------- /res/icons/action_cancelled.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 61 | 68 | 69 | -------------------------------------------------------------------------------- /res/icons/mobile.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 66 | 72 | 73 | -------------------------------------------------------------------------------- /res/icons/state_terminated.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 64 | 71 | 72 | -------------------------------------------------------------------------------- /res/icons/status_shut_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 64 | 71 | 72 | -------------------------------------------------------------------------------- /src/documentproxymodel.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include "src/documentproxymodel.h" 4 | 5 | DocumentProxyModel::DocumentProxyModel(DocumentsModel *docModel, QObject *parent) 6 | : QSortFilterProxyModel(parent), model_{docModel} 7 | { 8 | setSourceModel(model_); 9 | } 10 | 11 | QVariant DocumentProxyModel::data(const QModelIndex &ix, int role) const 12 | { 13 | static const int h_type = model_->fieldIndex("type"); 14 | static const int h_added_date = model_->fieldIndex("added_date"); 15 | static const int h_cls = model_->fieldIndex("cls"); 16 | static const int h_direction = model_->fieldIndex("direction"); 17 | static const int h_entity = model_->fieldIndex("entity"); 18 | static const int h_file_date = model_->fieldIndex("file_date"); 19 | 20 | if (role == Qt::DisplayRole || role == Qt::EditRole) { 21 | if (ix.column() == h_added_date || ix.column() == h_file_date) { 22 | const auto when = model_->data(ix, Qt::DisplayRole).toDateTime(); 23 | return when.date(); 24 | } 25 | 26 | // TODO: Map person, intent, action to name 27 | 28 | if (ix.column() == h_type) { 29 | return Document::typeName(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 30 | } 31 | 32 | if (ix.column() == h_cls) { 33 | return Document::className(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 34 | } 35 | 36 | if (ix.column() == h_direction) { 37 | return Document::directionName(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 38 | } 39 | 40 | if (ix.column() == h_entity) { 41 | return Document::entityName(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 42 | } 43 | 44 | } 45 | 46 | return model_->data(ix, role); 47 | } 48 | 49 | 50 | Qt::ItemFlags DocumentProxyModel::flags(const QModelIndex &ix) const 51 | { 52 | static const int h_name = model_->fieldIndex("name"); 53 | 54 | if (ix.isValid()) { 55 | if ((ix.column() == h_name)) { 56 | return QSortFilterProxyModel::flags(ix); 57 | } 58 | } 59 | 60 | 61 | return QSortFilterProxyModel::flags(ix) & ~Qt::EditRole; 62 | } 63 | -------------------------------------------------------------------------------- /src/intentdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "src/intentdialog.h" 2 | #include "ui_intentdialog.h" 3 | #include "intent.h" 4 | 5 | IntentDialog::IntentDialog(QWidget *parent) : 6 | QDialog(parent), 7 | ui(new Ui::IntentDialog) 8 | { 9 | ui->setupUi(this); 10 | 11 | for(auto e : GetIntentStateEnums()) { 12 | ui->state->addItem(GetIntentStateIcon(e), 13 | GetIntentStateName(e), 14 | static_cast(e)); 15 | } 16 | } 17 | 18 | IntentDialog::~IntentDialog() 19 | { 20 | delete ui; 21 | } 22 | 23 | void IntentDialog::setRecord(const QSqlRecord &rec) 24 | { 25 | rec_ = rec; 26 | } 27 | 28 | void IntentDialog::setModel(IntentsModel *model, QModelIndex &ix) 29 | { 30 | Q_ASSERT(mapper_ == nullptr); 31 | Q_ASSERT(model); 32 | Q_ASSERT(ix.isValid()); 33 | 34 | mapper_ = new QDataWidgetMapper(this); 35 | 36 | mapper_->setModel(model); 37 | mapper_->addMapping(ui->state, model->fieldIndex("state"), "currentData"); 38 | mapper_->addMapping(ui->abstract, model->fieldIndex("abstract")); 39 | mapper_->addMapping(ui->notes, model->fieldIndex("notes")); 40 | mapper_->addMapping(ui->createdDate, model->fieldIndex("created_date")); 41 | 42 | mapper_->setCurrentIndex(ix.row()); 43 | 44 | // The mapping is not smart enough to initialize combo boxes 45 | { 46 | const auto dix = model->index(ix.row(), model->property("state_col").toInt(), {}); 47 | int state_val = model->data(dix, Qt::EditRole).toInt(); 48 | ui->state->setCurrentIndex(state_val); 49 | } 50 | } 51 | 52 | 53 | void IntentDialog::accept() 54 | { 55 | if (!rec_.isEmpty()) { 56 | rec_.setValue("state", ui->state->currentData().toInt()); 57 | rec_.setValue("abstract", ui->abstract->text()); 58 | rec_.setValue("notes", ui->notes->toPlainText()); 59 | rec_.setValue("created_date", ui->createdDate->dateTime()); 60 | 61 | emit addIntent(rec_); 62 | } 63 | 64 | if (mapper_) { 65 | mapper_->submit(); 66 | } 67 | 68 | QDialog::accept(); 69 | } 70 | 71 | void IntentDialog::reject() 72 | { 73 | if (mapper_) { 74 | mapper_->revert(); 75 | } 76 | 77 | QDialog::reject(); 78 | } 79 | -------------------------------------------------------------------------------- /res/icons/request.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 59 | 63 | 66 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "logging.h" 2 | #include "mainwindow.h" 3 | #include "version.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | void initSettings() { 11 | 12 | QSettings settings; 13 | 14 | qDebug() << "Settings are in " << settings.fileName(); 15 | 16 | auto data_path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); 17 | if (!QDir(data_path).exists()) { 18 | qDebug() << "Creating path: " << data_path; 19 | QDir().mkpath(data_path); 20 | } 21 | 22 | switch(auto version = settings.value("version").toInt()) { 23 | case 0: { // First use 24 | 25 | qInfo() << "First use - initializing settings."; 26 | settings.setValue("version", 1); 27 | } break; 28 | default: 29 | qDebug() << "Settings are OK at version " << version; 30 | } 31 | 32 | if (settings.value("dbpath", "").toString().isEmpty()) { 33 | QString dbpath = data_path; 34 | #ifdef QT_DEBUG 35 | dbpath += "/f-crm-debug.db"; 36 | #else 37 | dbpath += "/f-crm.db"; 38 | #endif 39 | settings.setValue("dbpath", dbpath); 40 | } 41 | 42 | if (settings.value("log-path", "").toString().isEmpty()) { 43 | QString logpath = data_path; 44 | #ifdef QT_DEBUG 45 | logpath += "/f-crm-debug.log"; 46 | #else 47 | logpath += "/f-crm.log"; 48 | #endif 49 | settings.setValue("log-path", logpath); 50 | } 51 | } 52 | 53 | 54 | int main(int argc, char *argv[]) 55 | { 56 | QApplication a(argc, argv); 57 | 58 | a.setOrganizationName("TheLastViking"); 59 | a.setOrganizationDomain("lastviking.eu"); 60 | 61 | #ifdef QT_DEBUG 62 | a.setApplicationName("f-crm-debug"); 63 | #else 64 | a.setApplicationName("f-crm"); 65 | #endif 66 | initSettings(); 67 | 68 | Logging logger; 69 | qInstallMessageHandler(Logging::logMessageHandler); 70 | 71 | MainWindow w; 72 | try { 73 | w.initialize(); 74 | } catch(const std::exception& ex) { 75 | qWarning() << "Caught exception during initialization: " 76 | << ex.what(); 77 | return -1; 78 | } 79 | 80 | w.show(); 81 | 82 | return a.exec(); 83 | } 84 | -------------------------------------------------------------------------------- /res/icons/execute_action.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 61 | 65 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /res/icons/internal.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 58 | 63 | 64 | 70 | 76 | 77 | -------------------------------------------------------------------------------- /res/icons/action_on_hold.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 60 | 65 | 71 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/intentproxymodel.cpp: -------------------------------------------------------------------------------- 1 | #include "src/intentproxymodel.h" 2 | #include 3 | 4 | 5 | IntentProxyModel::IntentProxyModel(IntentsModel *docModel, QObject *parent) 6 | : QSortFilterProxyModel(parent), model_{docModel} 7 | { 8 | setSourceModel(model_); 9 | } 10 | 11 | QVariant IntentProxyModel::data(const QModelIndex &ix, int role) const 12 | { 13 | static const int h_state = model_->fieldIndex("state"); 14 | static const int h_type = model_->fieldIndex("type"); 15 | static const int h_abstract = model_->fieldIndex("abstract"); 16 | static const int h_created_date = model_->fieldIndex("created_date"); 17 | 18 | if (ix.isValid()) { 19 | if (role == Qt::DisplayRole || role == Qt::EditRole) { 20 | 21 | if (ix.column() == h_type) { 22 | return GetIntentTypeName(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 23 | } 24 | 25 | if (ix.column() == h_state) { 26 | return GetIntentStateName(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 27 | } 28 | 29 | if (role == Qt::DisplayRole) { 30 | if (ix.column() == h_created_date) { 31 | return model_->data(ix, role).toDate(); 32 | } 33 | } 34 | 35 | } else if (role == Qt::DecorationRole) { 36 | 37 | if (ix.column() == h_abstract) { 38 | const auto cix = index(ix.row(), h_type, {}); 39 | return GetIntentTypeIcon(std::max(0, model_->data(cix, Qt::DisplayRole).toInt())); 40 | } 41 | 42 | if (ix.column() == h_type) { 43 | return GetIntentTypeIcon(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 44 | } 45 | 46 | if (ix.column() == h_state) { 47 | return GetIntentStateIcon(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 48 | } 49 | } 50 | } 51 | 52 | return model_->data(ix, role); 53 | } 54 | 55 | 56 | Qt::ItemFlags IntentProxyModel::flags(const QModelIndex &ix) const 57 | { 58 | static const int h_abstract = model_->fieldIndex("abstract"); 59 | 60 | if (ix.isValid()) { 61 | if ((ix.column() == h_abstract)) { 62 | return QSortFilterProxyModel::flags(ix); 63 | } 64 | } 65 | 66 | return QSortFilterProxyModel::flags(ix) & ~Qt::EditRole; 67 | } 68 | -------------------------------------------------------------------------------- /res/icons/clear_filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 60 | 67 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/channel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "channel.h" 4 | 5 | 6 | using namespace std; 7 | 8 | QIcon GetChannelStatusIcon(const ChannelType type) 9 | { 10 | return GetChannelStatusIcon(static_cast(type)); 11 | } 12 | 13 | QIcon GetChannelStatusIcon(const int type) 14 | { 15 | static const array icons{{ 16 | QIcon(":/res/icons/ch_type_other.svg"), 17 | QIcon(":/res/icons/ch_type_web.svg"), 18 | QIcon(":/res/icons/mail.svg"), 19 | QIcon(":/res/icons/phone.svg"), 20 | QIcon(":/res/icons/mobile.svg"), 21 | QIcon(":/res/icons/skype.svg"), 22 | QIcon(":/res/icons/linkedin.svg"), 23 | QIcon(":/res/icons/reddit.svg"), 24 | QIcon(":/res/icons/facebook.svg"), 25 | QIcon(":/res/icons/github.svg") 26 | }}; 27 | 28 | return icons.at(static_cast(type)); 29 | } 30 | 31 | ChannelType ToChannelType(const int type) 32 | { 33 | static const array types {{ 34 | ChannelType::OTHER, 35 | ChannelType::WEB, 36 | ChannelType::EMAIL, 37 | ChannelType::PHONE, 38 | ChannelType::MOBILE, 39 | ChannelType::SKYPE, 40 | ChannelType::LINKEDIN, 41 | ChannelType::REDDIT, 42 | ChannelType::FACEBOOK, 43 | ChannelType::GITHUB 44 | }}; 45 | 46 | return types.at(static_cast(type)); 47 | } 48 | 49 | QString GetChannelTypeName(const ChannelType type) 50 | { 51 | return GetChannelTypeName(static_cast(type)); 52 | } 53 | 54 | QString GetChannelTypeName(const int type) 55 | { 56 | static const array names {{ 57 | "Other", 58 | "Web", 59 | "Email", 60 | "Phone", 61 | "Mobile", 62 | "Skype", 63 | "Linkedin", 64 | "Reddit", 65 | "Facebook", 66 | "Github", 67 | }}; 68 | 69 | return names.at(static_cast(type)); 70 | } 71 | 72 | const std::array &GetChannelTypeEnums() 73 | { 74 | static const std::array enums {{ 75 | ChannelType::OTHER, 76 | ChannelType::WEB, 77 | ChannelType::EMAIL, 78 | ChannelType::PHONE, 79 | ChannelType::MOBILE, 80 | ChannelType::SKYPE, 81 | ChannelType::LINKEDIN, 82 | ChannelType::REDDIT, 83 | ChannelType::FACEBOOK, 84 | ChannelType::GITHUB 85 | }}; 86 | 87 | return enums; 88 | } 89 | -------------------------------------------------------------------------------- /res/icons/gender_unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 61 | ? 72 | 73 | -------------------------------------------------------------------------------- /res/icons/action_type_meeting.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 59 | 63 | 68 | 69 | 76 | 77 | -------------------------------------------------------------------------------- /src/actionsmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef ACTIONSMODEL_H 2 | #define ACTIONSMODEL_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "database.h" 12 | 13 | // Create read-only properties like 'name_col' for the database columns 14 | #define DEF_COLUMN(name) Q_PROPERTY(int name ## _col MEMBER h_ ## name ## _) 15 | 16 | 17 | class ActionsModel : public QSqlTableModel 18 | { 19 | Q_OBJECT 20 | public: 21 | ActionsModel(QSettings& settings, QObject *parent, QSqlDatabase db); 22 | 23 | DEF_COLUMN(id) 24 | DEF_COLUMN(sequence) 25 | DEF_COLUMN(intent) 26 | DEF_COLUMN(contact) 27 | DEF_COLUMN(person) 28 | DEF_COLUMN(state) 29 | DEF_COLUMN(type) 30 | DEF_COLUMN(channel_type) 31 | DEF_COLUMN(name) 32 | DEF_COLUMN(created_date) 33 | DEF_COLUMN(start_date) 34 | DEF_COLUMN(due_date) 35 | DEF_COLUMN(desired_outcome) 36 | DEF_COLUMN(notes) 37 | 38 | void setContact(int id); 39 | void setIntent(int id); 40 | int contact() const { return contact_; } 41 | 42 | // Get a record with default values 43 | QSqlRecord getRecord(); 44 | 45 | public slots: 46 | void removeActions(const QModelIndexList& indexes); 47 | void addAction(const QSqlRecord& rec); 48 | void setCompleted(const QModelIndex& ix); 49 | void moveUp(const QModelIndex& ix); 50 | void moveDown(const QModelIndex& ix); 51 | void openNextActions(); 52 | void updateState(); 53 | 54 | private: 55 | void doMove(const QModelIndex &ix, const int offset); 56 | 57 | QSettings& settings_; 58 | 59 | int h_id_ = {}; 60 | int h_sequence_ = {}; 61 | int h_contact_ = {}; 62 | int h_intent_ = {}; 63 | int h_person_ = {}; 64 | int h_state_ = {}; 65 | int h_type_ = {}; 66 | int h_channel_type_ = {}; 67 | int h_name_ = {}; 68 | int h_created_date_ = {}; 69 | int h_start_date_ = {}; 70 | int h_due_date_ = {}; 71 | int h_desired_outcome_ = {}; 72 | int h_notes_ = {}; 73 | 74 | // QAbstractItemModel interface 75 | public: 76 | QVariant data(const QModelIndex &index, int role) const override; 77 | QVariant headerData(int section, Qt::Orientation orientation, int role) const override; 78 | int contact_ = {}; 79 | int intent_ = {}; 80 | 81 | // QSqlTableModel interface 82 | protected: 83 | bool updateRowInTable(int row, const QSqlRecord &values) override; 84 | }; 85 | 86 | 87 | #undef DEF_COLUMN 88 | 89 | #endif // ACTIONSMODEL_H 90 | -------------------------------------------------------------------------------- /src/journalmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGMODEL_H 2 | #define LOGMODEL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "database.h" 11 | 12 | // Create read-only properties like 'name_col' for the database columns 13 | #define DEF_COLUMN(name) Q_PROPERTY(int name ## _col MEMBER h_ ## name ## _) 14 | 15 | 16 | class JournalModel : public QSqlTableModel 17 | { 18 | Q_OBJECT 19 | public: 20 | enum class Type { 21 | GENERAL, 22 | ADD_COMPANY, 23 | ADD_PERSON, 24 | UPDATED_CONTACT, 25 | UPDATED_PERSON, 26 | ADD_DOCUMENT, 27 | UPDATED_DOCUMENT, 28 | DELETED_DOCUMENT, 29 | ADD_INTENT, 30 | UPDATE_INTENT, 31 | DELETE_INTENT, 32 | ADD_ACTION, 33 | EDIT_ACTION, 34 | DELETE_ACTION, 35 | DELETED_SOMETHING, 36 | }; 37 | 38 | JournalModel(QSettings& settings, QObject *parent, QSqlDatabase db); 39 | 40 | DEF_COLUMN(id) 41 | DEF_COLUMN(type) 42 | DEF_COLUMN(date) 43 | DEF_COLUMN(contact) 44 | DEF_COLUMN(person) 45 | DEF_COLUMN(intent) 46 | DEF_COLUMN(channel) 47 | DEF_COLUMN(activity) 48 | DEF_COLUMN(document) 49 | DEF_COLUMN(text) 50 | 51 | void setContact(int id); 52 | 53 | static JournalModel& instance() { 54 | Q_ASSERT(instance_); 55 | return *instance_; 56 | } 57 | 58 | public slots: 59 | void addEntry(QSqlRecord& rec); // Will modify rec 60 | //void addContactLog(const int contact, const Type type, const QString& text); 61 | void addEntry(const Type type, const QString& text, 62 | const int contact, const int person = 0, const int intent = 0, 63 | const int activity = 0, const int document = 0); 64 | 65 | private: 66 | const QIcon& getLogIcon(int type) const; 67 | 68 | QSettings& settings_; 69 | static JournalModel *instance_; 70 | 71 | int h_id_ = {}; 72 | int h_type_ = {}; 73 | int h_date_ = {}; 74 | int h_contact_ = {}; 75 | int h_person_ = {}; 76 | int h_intent_ = {}; 77 | int h_channel_ = {}; 78 | int h_activity_ = {}; 79 | int h_document_ = {}; 80 | int h_text_ = {}; 81 | 82 | // QAbstractItemModel interface 83 | public: 84 | QVariant data(const QModelIndex &index, int role) const override; 85 | QVariant headerData(int section, Qt::Orientation orientation, int role) const override; 86 | }; 87 | 88 | 89 | #undef DEF_COLUMN 90 | 91 | #endif // LOGMODEL_H 92 | -------------------------------------------------------------------------------- /src/documentsmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTSMODEL_H 2 | #define DOCUMENTSMODEL_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "database.h" 12 | #include "document.h" 13 | 14 | // Create read-only properties like 'name_col' for the database columns 15 | #define DEF_COLUMN(name) Q_PROPERTY(int name ## _col MEMBER h_ ## name ## _) 16 | 17 | 18 | class DocumentsModel : public QSqlTableModel 19 | { 20 | Q_OBJECT 21 | public: 22 | DocumentsModel(QSettings& settings, QObject *parent, QSqlDatabase db); 23 | 24 | DEF_COLUMN(id) 25 | DEF_COLUMN(contact) 26 | DEF_COLUMN(person) 27 | DEF_COLUMN(intent) 28 | DEF_COLUMN(activity) 29 | DEF_COLUMN(type) 30 | DEF_COLUMN(cls) 31 | DEF_COLUMN(direction) 32 | DEF_COLUMN(entity) 33 | DEF_COLUMN(name) 34 | DEF_COLUMN(notes) 35 | DEF_COLUMN(added_date) 36 | DEF_COLUMN(file_date) 37 | DEF_COLUMN(location) 38 | DEF_COLUMN(content) 39 | 40 | void setContact(int id); 41 | 42 | // Get a record with default values 43 | QSqlRecord getRecord(int contact, Document::Type type, 44 | Document::Class cls, Document::Direction direction, 45 | Document::Entity entity = Document::Entity::CONTACT, 46 | int person = 0, int intent = 0, int action = 0); 47 | 48 | public slots: 49 | void removeDocuments(const QModelIndexList& indexes); 50 | void addDocument(const QSqlRecord& rec); 51 | void updateDocument(const int row, const QSqlRecord& rec); 52 | 53 | private: 54 | void fix(QSqlRecord& rec); 55 | 56 | 57 | QSettings& settings_; 58 | 59 | int h_id_ = {}; 60 | int h_contact_ = {}; 61 | int h_person_ = {}; 62 | int h_intent_ = {}; 63 | int h_activity_ = {}; 64 | int h_type_ = {}; 65 | int h_cls_ = {}; 66 | int h_direction_ = {}; 67 | int h_entity_ = {}; 68 | int h_name_ = {}; 69 | int h_notes_ = {}; 70 | int h_added_date_ = {}; 71 | int h_file_date_ = {}; 72 | int h_location_ = {}; 73 | int h_content_ = {}; 74 | 75 | // QAbstractItemModel interface 76 | public: 77 | QVariant data(const QModelIndex &index, int role) const override; 78 | QVariant headerData(int section, Qt::Orientation orientation, int role) const override; 79 | 80 | // QSqlTableModel interface 81 | protected: 82 | bool updateRowInTable(int row, const QSqlRecord &values) override; 83 | }; 84 | 85 | 86 | #endif // DOCUMENTSMODEL_H 87 | -------------------------------------------------------------------------------- /res/icons/state_failed.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 64 | 68 | 75 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /res/icons/status_lost.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 64 | 68 | 75 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /res/icons/action_failed.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 61 | 65 | 72 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /res/icons/status_banned.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 61 | 63 | 70 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ci/jenkins/Dockefile.ubuntu-trusty: -------------------------------------------------------------------------------- 1 | # Based on: https://github.com/evarga/docker-images/blob/master/jenkins-slave/Dockerfile 2 | FROM ubuntu:trusty 3 | 4 | MAINTAINER Jarle Aase 5 | 6 | # In case you need proxy 7 | #RUN echo 'Acquire::http::Proxy "http://127.0.0.1:8080";' >> /etc/apt/apt.conf 8 | 9 | RUN apt-get -q update &&\ 10 | DEBIAN_FRONTEND="noninteractive" apt-get -q upgrade -y -o Dpkg::Options::="--force-confnew" --no-install-recommends &&\ 11 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends openssh-server software-properties-common &&\ 12 | DEBIAN_FRONTEND="noninteractive" add-apt-repository ppa:beineri/opt-qt-5.10.1-trusty &&\ 13 | DEBIAN_FRONTEND="noninteractive" add-apt-repository ppa:ubuntu-toolchain-r/test -y &&\ 14 | apt-get -q update &&\ 15 | DEBIAN_FRONTEND="noninteractive" apt-get install -y qt510-meta-full qt510base qt510declarative qt510tools qt510multimedia qt510svg qt510graphicaleffects qt510quickcontrols qt510quickcontrols2 qt510sensors qt510connectivity qt510location qt510websockets qt510webchannel qt5103d qt510canvas3d qt510scxml qt510webengine &&\ 16 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y g++ git make \ 17 | qt510-meta-full libgl1-mesa-dev ruby build-essential curl gcc-7 g++-7 libfuse2 fuse &&\ 18 | update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 60 --slave /usr/bin/g++ g++ /usr/bin/g++-7 &&\ 19 | sed -i 's|session required pam_loginuid.so|session optional pam_loginuid.so|g' /etc/pam.d/sshd &&\ 20 | mkdir -p /var/run/sshd &&\ 21 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends software-properties-common &&\ 22 | add-apt-repository -y ppa:openjdk-r/ppa &&\ 23 | apt-get -q update &&\ 24 | DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends openjdk-8-jre-headless &&\ 25 | apt-get -q clean -y && rm -rf /var/lib/apt/lists/* && rm -f /var/cache/apt/*.bin &&\ 26 | curl -L -o /usr/local/bin/linuxdeployqt https://github.com/probonopd/linuxdeployqt/releases/download/5/linuxdeployqt-5-x86_64.AppImage &&\ 27 | chmod +x /usr/local/bin/linuxdeployqt 28 | 29 | # Set user jenkins to the image 30 | # We need to set the same uid as user "jenkins" have on the host machine 31 | RUN useradd -u 111 -m -d /home/jenkins -s /bin/bash jenkins &&\ 32 | echo "jenkins:jenkins" | chpasswd 33 | 34 | RUN adduser jenkins fuse 35 | RUN chmod a+r /etc/fuse.conf 36 | 37 | # Standard SSH port 38 | EXPOSE 22 39 | 40 | # Default command 41 | CMD ["/usr/sbin/sshd", "-D"] 42 | -------------------------------------------------------------------------------- /res/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 40 | 44 | 45 | 46 | 66 | 70 | 74 | 81 | 88 | 89 | -------------------------------------------------------------------------------- /res/icons/action_done.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 59 | 62 | 67 | 68 | -------------------------------------------------------------------------------- /res/icons/state_succeeded.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 59 | 62 | 67 | 68 | -------------------------------------------------------------------------------- /ui/actionexecutedialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ActionExecuteDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 184 11 | 12 | 13 | 14 | Execute Action 15 | 16 | 17 | 18 | 19 | 20 | Execute the action by opening the following channel: 21 | 22 | 23 | 24 | 25 | 26 | 27 | -1 28 | 29 | 30 | true 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Qt::Horizontal 40 | 41 | 42 | 43 | 40 44 | 20 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Qt::Horizontal 53 | 54 | 55 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | buttonBox 67 | accepted() 68 | ActionExecuteDialog 69 | accept() 70 | 71 | 72 | 248 73 | 254 74 | 75 | 76 | 157 77 | 274 78 | 79 | 80 | 81 | 82 | buttonBox 83 | rejected() 84 | ActionExecuteDialog 85 | reject() 86 | 87 | 88 | 316 89 | 260 90 | 91 | 92 | 286 93 | 274 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /scripts/package-appimage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | APP="f-crm" 4 | ARCH="x86_64" 5 | 6 | if [ -z "$F_CRM_VERSION" ]; then 7 | F_CRM_VERSION="2.0.0" 8 | echo "Warning: Missing F_CRM_VERSION variable!" 9 | fi 10 | 11 | if [ -z ${DIST_DIR:-} ]; 12 | then 13 | DIST_DIR=`pwd`/dist/linux-appimage 14 | fi 15 | 16 | if [ -z ${SIGN_CERT:-} ]; 17 | then 18 | SIGN_CERT="Developer ID Application" 19 | fi 20 | 21 | if [ -z ${BUILD_DIR:-} ]; then 22 | BUILD_DIR=`pwd`/build 23 | fi 24 | 25 | if [ -z ${BUILD_DIR:-} ]; then 26 | BUILD_DIR=`pwd`/build 27 | fi 28 | 29 | if [ -z ${SRC_DIR:-} ]; 30 | then 31 | # Just assume we are run from the scipts directory 32 | SRC_DIR=`pwd`/.. 33 | fi 34 | 35 | if [ -z ${QTDIR:-} ]; 36 | then 37 | QMAKE=qmake 38 | else 39 | export QT_LIBRARY_PATH=$QTDIR/lib 40 | export QT_PLUGIN_PATH=$QTDIR/plugins/ 41 | QMAKE=$QTDIR/bin/qmake 42 | fi 43 | 44 | echo "Building ${APP} for APPIMAGE into ${DIST_DIR} from ${SRC_DIR}" 45 | 46 | rm -rf $DIST_DIR $BUILD_DIR 47 | 48 | mkdir -p $DIST_DIR && cd $DIST_DIR 49 | mkdir -p $BUILD_DIR 50 | 51 | pushd $BUILD_DIR 52 | BUILD_DIR=$(pwd) 53 | 54 | $QMAKE \ 55 | -spec linux-g++ \ 56 | "CONFIG += release x86_64" \ 57 | $SRC_DIR/${APP}.pro 58 | 59 | if ! make -j10 ; 60 | then 61 | exit 1 62 | fi 63 | 64 | popd 65 | 66 | pushd $DIST_DIR 67 | 68 | APPIMAGE_DIR=$(pwd)/AppImage 69 | 70 | echo "Constructing AppImage layout in: ${APPIMAGE_DIR}" 71 | 72 | mkdir -p $APPIMAGE_DIR/usr/bin 73 | if ! cp -v $BUILD_DIR/${APP} $APPIMAGE_DIR/usr/bin ; 74 | then 75 | exit 1 76 | fi 77 | mkdir -p $APPIMAGE_DIR/usr/lib 78 | mkdir -p $APPIMAGE_DIR/usr/share/applications 79 | printf "[Desktop Entry]\nType=Application\nName=${APP}\nExec=${APP}\nIcon=${APP}\nCategories=Office;" > $APPIMAGE_DIR/usr/share/applications/${APP}.desktop 80 | mkdir -p $APPIMAGE_DIR/usr/share/icons/default/scalable/apps 81 | cp $SRC_DIR/res/icons/${APP}.svg $APPIMAGE_DIR/usr/share/icons/default/scalable/apps 82 | 83 | pushd ${APPIMAGE_DIR} 84 | 85 | echo "Diagnostics information" 86 | pwd 87 | ls -la /etc/fuse.conf 88 | ls -la /dev/fuse 89 | echo "who: ${USER}" 90 | whoami 91 | echo "fusermount" 92 | fusermount -V 93 | echo "grep grops" 94 | grep jenkins /etc/group 95 | echo "grep users" 96 | grep jenkins /etc/passwd 97 | 98 | if ! linuxdeployqt \ 99 | $APPIMAGE_DIR/usr/share/applications/${APP}.desktop \ 100 | -qmake=$QMAKE \ 101 | -exclude-libs=libqsqlmysql,libqsqlpsql \ 102 | -extra-plugins=iconengines,imageformats \ 103 | -appimage ; 104 | then 105 | exit 1 106 | fi 107 | 108 | mv ${APP}-${ARCH}.AppImage $DIST_DIR/${APP}-${ARCH}-${F_CRM_VERSION}.AppImage 109 | 110 | popd 111 | popd 112 | -------------------------------------------------------------------------------- /src/document.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENT_H 2 | #define DOCUMENT_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | struct Document 10 | { 11 | enum class Type { 12 | NOTE, // Internal in the database 13 | EMAIL, 14 | URL, 15 | FILE 16 | }; 17 | 18 | static constexpr size_t type_enums{4}; 19 | using type_enums_t = std::array; 20 | 21 | static const QIcon& typeIcon(const Type value); 22 | static const QIcon& typeIcon(const int value); 23 | static Type toType(const int value); 24 | static const QString& typeName(const Type value); 25 | static const QString& typeName(const int value); 26 | static const type_enums_t& typeEnums(); 27 | 28 | 29 | enum class Class 30 | { 31 | RESEARCH, 32 | NOTE, 33 | PROPOSAL, 34 | REQUEST, 35 | OFFER 36 | }; 37 | 38 | static constexpr size_t class_enums{5}; 39 | using class_enums_t = std::array; 40 | 41 | static const QIcon& classIcon(const Class value); 42 | static const QIcon& classIcon(const int value); 43 | static Class toClass(const int value); 44 | static const QString& className(const Class value); 45 | static const QString& className(const int value); 46 | static const class_enums_t& classEnums(); 47 | 48 | enum class Direction 49 | { 50 | INTERNAL, 51 | INCOMING, 52 | OUTGOING 53 | }; 54 | 55 | static constexpr size_t direction_enums{3}; 56 | using direction_enums_t = std::array; 57 | 58 | static const QIcon& directionIcon(const Direction value); 59 | static const QIcon& directionIcon(const int value); 60 | static Direction toDirection(const int value); 61 | static const QString& directionName(const Direction value); 62 | static const QString& directionName(const int value); 63 | static const direction_enums_t& directionEnums(); 64 | 65 | enum class Entity 66 | { 67 | CONTACT, 68 | PERSON, 69 | INTENT, 70 | ACTION 71 | }; 72 | 73 | static constexpr size_t entity_enums{4}; 74 | using entity_enums_t = std::array; 75 | 76 | static const QIcon& entityIcon(const Entity value); 77 | static const QIcon& entityIcon(const int value); 78 | static Entity toEntity(const int value); 79 | static const QString& entityName(const Entity value); 80 | static const QString& entityName(const int value); 81 | static const entity_enums_t& entityEnums(); 82 | 83 | static void open(Type type, QString value); 84 | static void openFile(QString path); 85 | static void openUrl(QString url); 86 | static Type deduceType(const QUrl& url); 87 | }; 88 | 89 | #endif // DOCUMENT_H 90 | -------------------------------------------------------------------------------- /res/icons/action_blocked.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 61 | 64 | 69 | 74 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/contactsmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTACTSMODEL_H 2 | #define CONTACTSMODEL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "database.h" 11 | #include "contact.h" 12 | 13 | // Create read-only properties like 'name_col' for the database columns 14 | #define DEF_COLUMN(name) Q_PROPERTY(int name ## _col MEMBER h_ ## name ## _) 15 | 16 | class ContactsModel : public QSqlTableModel 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | ContactsModel(QSettings& settings, QObject *parent, QSqlDatabase db); 22 | ~ContactsModel() = default; 23 | 24 | DEF_COLUMN(id) 25 | DEF_COLUMN(contact) 26 | DEF_COLUMN(created_date) 27 | DEF_COLUMN(last_activity_date) 28 | DEF_COLUMN(name) 29 | DEF_COLUMN(gender) 30 | DEF_COLUMN(type) 31 | DEF_COLUMN(status) 32 | DEF_COLUMN(notes) 33 | DEF_COLUMN(stars) 34 | DEF_COLUMN(favorite) 35 | DEF_COLUMN(address1) 36 | DEF_COLUMN(address2) 37 | DEF_COLUMN(city) 38 | DEF_COLUMN(postcode) 39 | DEF_COLUMN(country) 40 | 41 | QModelIndex createContact(const ContactType type); 42 | 43 | public: 44 | QVariant data(const QModelIndex &index, int role) const override; 45 | QVariant headerData(int section, Qt::Orientation orientation, int role) const override; 46 | Qt::ItemFlags flags(const QModelIndex &index) const override; 47 | int getContactId(const QModelIndex& ix) const; 48 | 49 | public slots: 50 | void setNameFilter(const QString& filter); 51 | 52 | // If used, this instance is for contacts belonging to a company (parent contact) 53 | void setParent(int contact); 54 | void removeContacts(const QModelIndexList& indexes); 55 | void addPerson(const QSqlRecord& rec); 56 | void toggleFavoriteStatus(const int row); 57 | void setStars(const int row, const int stars); 58 | 59 | private: 60 | bool insertContact(QSqlRecord& rec); 61 | static const QIcon& getFavoriteIcon(const bool enable); 62 | static const QIcon& getStars(const int stars); 63 | 64 | QSettings& settings_; 65 | 66 | int h_id_ = {}; 67 | int h_contact_ = {}; 68 | int h_created_date_ = {}; 69 | int h_last_activity_date_ = {}; 70 | int h_name_ = {}; 71 | int h_gender_ = {}; 72 | int h_type_ = {}; 73 | int h_status_ = {}; 74 | int h_notes_ = {}; 75 | int h_stars_ = {}; 76 | int h_favorite_ = {}; 77 | int h_address1_ = {}; 78 | int h_address2_ = {}; 79 | int h_city_ = {}; 80 | int h_postcode_ = {}; 81 | int h_region_ = {}; 82 | int h_state_ = {}; // State as in Alabama, 83 | int h_country_ = {}; 84 | 85 | mutable bool internal_edit_ = false; 86 | int parent_ = {}; 87 | }; 88 | 89 | #undef DEF_COLUMN 90 | 91 | #endif // CONTACTSMODEL_H 92 | -------------------------------------------------------------------------------- /src/intent.cpp: -------------------------------------------------------------------------------- 1 | #include "src/intent.h" 2 | 3 | using namespace std; 4 | 5 | 6 | QIcon GetIntentTypeIcon(const IntentType type) 7 | { 8 | return GetIntentTypeIcon(static_cast(type)); 9 | } 10 | 11 | 12 | QIcon GetIntentTypeIcon(const int type) 13 | { 14 | static const array icons {{ 15 | QIcon(":/res/icons/intent_type_manual.svg"), 16 | }}; 17 | 18 | return icons.at(static_cast(type)); 19 | } 20 | 21 | IntentType ToIntentType(const int type) 22 | { 23 | return GetIntentTypeEnums().at(static_cast(type)); 24 | } 25 | 26 | const QString &GetIntentTypeName(const IntentType type) 27 | { 28 | return GetIntentTypeName(static_cast(type)); 29 | } 30 | 31 | const QString &GetIntentTypeName(const int type) 32 | { 33 | static const array names {{ 34 | "Manual", 35 | }}; 36 | 37 | return names.at(static_cast(type)); 38 | } 39 | 40 | const std::array& GetIntentTypeEnums() 41 | { 42 | static const std::array enums {{ 43 | IntentType::MANUAL, 44 | }}; 45 | 46 | return enums; 47 | } 48 | 49 | 50 | 51 | 52 | QIcon GetIntentStateIcon(const IntentState type) 53 | { 54 | return GetIntentStateIcon(static_cast(type)); 55 | } 56 | 57 | 58 | QIcon GetIntentStateIcon(const int type) 59 | { 60 | static const array icons {{ 61 | QIcon(":/res/icons/state_defined.svg"), 62 | QIcon(":/res/icons/state_progress.svg"), 63 | QIcon(":/res/icons/state_succeeded.svg"), 64 | QIcon(":/res/icons/state_failed.svg"), 65 | QIcon(":/res/icons/state_terminated.svg"), 66 | 67 | }}; 68 | 69 | return icons.at(static_cast(type)); 70 | } 71 | 72 | IntentState ToIntentState(const int type) 73 | { 74 | return GetIntentStateEnums().at(static_cast(type)); 75 | } 76 | 77 | const QString &GetIntentStateName(const IntentState type) 78 | { 79 | return GetIntentStateName(static_cast(type)); 80 | } 81 | 82 | const QString &GetIntentStateName(const int type) 83 | { 84 | static const array names {{ 85 | "Defined", 86 | "Progress", 87 | "Succeeded", 88 | "Failed", 89 | "Terminated" 90 | }}; 91 | 92 | return names.at(static_cast(type)); 93 | } 94 | 95 | const std::array& GetIntentStateEnums() 96 | { 97 | static const std::array enums {{ 98 | IntentState::DEFINED, 99 | IntentState::PROGRESS, 100 | IntentState::SUCCEEDED, 101 | IntentState::FAILED, 102 | IntentState::TERMINATED 103 | }}; 104 | 105 | return enums; 106 | } 107 | -------------------------------------------------------------------------------- /res/icons/edit_action.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 40 | 44 | 45 | 48 | 52 | 53 | 54 | 74 | 79 | 83 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /res/icons/gender_male.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 61 | 62 | -------------------------------------------------------------------------------- /res/icons/reddit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /res/icons/gender_female.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 36 | 56 | 61 | 62 | -------------------------------------------------------------------------------- /res/icons/add_action.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 40 | 44 | 45 | 48 | 52 | 53 | 54 | 74 | 77 | 80 | 87 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /res/icons/addchannel.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 40 | 44 | 45 | 48 | 52 | 53 | 54 | 74 | 77 | 80 | 87 | 94 | 95 | -------------------------------------------------------------------------------- /src/settingsdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "src/settingsdialog.h" 2 | #include "ui_settingsdialog.h" 3 | #include "logging.h" 4 | 5 | #include 6 | 7 | SettingsDialog::SettingsDialog(QSettings& settings, QWidget *parent) : 8 | QDialog(parent), 9 | ui(new Ui::SettingsDialog), 10 | settings_{settings} 11 | { 12 | ui->setupUi(this); 13 | 14 | ui->dbPathEdit->setText(settings_.value("dbpath", "").toString()); 15 | ui->saveWindowState->setCheckState( 16 | settings_.value("restore-window-state", true).toBool() 17 | ? Qt::Checked : Qt::Unchecked); 18 | 19 | ui->logPathEdit->setText(settings_.value("log-path", "f-crm.log").toString()); 20 | ui->emailApp->setText(settings_.value("mailapp", "").toString()); 21 | 22 | ui->enableLoggingCheck->setCheckState( 23 | settings_.value("log-enabled", false).toBool() 24 | ? Qt::Checked : Qt::Unchecked); 25 | ui->logAppendCheck->setCheckState( 26 | settings.value("log-append", false).toBool() 27 | ? Qt::Checked : Qt::Unchecked); 28 | ui->logPathEdit->setText(settings_.value("log-path", "whid.log").toString()); 29 | 30 | ui->tabCtl->setCurrentIndex(0); 31 | 32 | connect(ui->dbSelectPathBtn, SIGNAL(clicked()), this, SLOT(selectDbFile())); 33 | connect(this, SIGNAL(logSettingsChanged()), Logging::instance(), SLOT(changed())); 34 | } 35 | 36 | SettingsDialog::~SettingsDialog() 37 | { 38 | delete ui; 39 | } 40 | 41 | 42 | void SettingsDialog::accept() 43 | { 44 | settings_.setValue("dbpath", ui->dbPathEdit->text()); 45 | settings_.setValue("mailapp", ui->emailApp->text()); 46 | 47 | settings_.setValue("restore-window-state", 48 | ui->saveWindowState->checkState() == Qt::Checked); 49 | 50 | bool log_changed = false; 51 | 52 | if (settings_.value("log-enabled",false) 53 | != (ui->enableLoggingCheck->checkState() == Qt::Checked)) { 54 | log_changed = true; 55 | } 56 | 57 | settings_.setValue("log-enabled", 58 | ui->enableLoggingCheck->checkState() == Qt::Checked); 59 | 60 | 61 | if (settings_.value("log-path") != ui->logPathEdit->text()) { 62 | log_changed = true; 63 | } 64 | 65 | settings_.setValue("log-path", ui->logPathEdit->text()); 66 | 67 | settings_.setValue("log-append", 68 | ui->logAppendCheck->checkState() == Qt::Checked); 69 | 70 | 71 | if (log_changed) { 72 | emit logSettingsChanged(); 73 | } 74 | 75 | QDialog::accept(); 76 | } 77 | 78 | void SettingsDialog::selectDbFile() 79 | { 80 | auto path = QFileDialog::getSaveFileName(this, 81 | "Select Datatabase", 82 | ui->dbPathEdit->text(), 83 | "SQLite Files (*.db)", 84 | Q_NULLPTR, 85 | QFileDialog::DontConfirmOverwrite); 86 | 87 | if (!path.isNull() && !path.isEmpty()) { 88 | ui->dbPathEdit->setText(path); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/action.cpp: -------------------------------------------------------------------------------- 1 | #include "src/action.h" 2 | 3 | using namespace std; 4 | 5 | QIcon GetActionStateIcon(const ActionState type) 6 | { 7 | return GetActionStateIcon(static_cast(type)); 8 | } 9 | 10 | 11 | QIcon GetActionStateIcon(const int type) 12 | { 13 | static const array icons {{ 14 | QIcon(":/res/icons/action_waiting.svg"), 15 | QIcon(":/res/icons/action_ready.svg"), 16 | QIcon(":/res/icons/action_blocked.svg"), 17 | QIcon(":/res/icons/action_on_hold.svg"), 18 | QIcon(":/res/icons/action_done.svg"), 19 | QIcon(":/res/icons/action_cancelled.svg"), 20 | QIcon(":/res/icons/action_failed.svg") 21 | }}; 22 | 23 | return icons.at(static_cast(type)); 24 | } 25 | 26 | ActionState ToActionState(const int type) 27 | { 28 | return GetActionStateEnums().at(static_cast(type)); 29 | } 30 | 31 | const QString &GetActionStateName(const ActionState type) 32 | { 33 | return GetActionStateName(static_cast(type)); 34 | } 35 | 36 | const QString &GetActionStateName(const int type) 37 | { 38 | static const array names {{ 39 | "Waiting", 40 | "Open", 41 | "Blocked", 42 | "On hold", 43 | "Done", 44 | "Cancelled", 45 | "Failed" 46 | }}; 47 | 48 | return names.at(static_cast(type)); 49 | } 50 | 51 | const std::array& GetActionStateEnums() 52 | { 53 | static const std::array enums {{ 54 | ActionState::WAITING, 55 | ActionState::OPEN, 56 | ActionState::BLOCKED, 57 | ActionState::ON_HOLD, 58 | ActionState::DONE, 59 | ActionState::CANCELLED, 60 | ActionState::FAILED 61 | }}; 62 | 63 | return enums; 64 | } 65 | 66 | 67 | 68 | QIcon GetActionTypeIcon(const ActionType type) 69 | { 70 | return GetActionTypeIcon(static_cast(type)); 71 | } 72 | 73 | 74 | QIcon GetActionTypeIcon(const int type) 75 | { 76 | static const array icons {{ 77 | QIcon(":/res/icons/action_type_task.svg"), 78 | QIcon(":/res/icons/action_type_channel.svg"), 79 | QIcon(":/res/icons/action_type_meeting.svg"), 80 | }}; 81 | 82 | return icons.at(static_cast(type)); 83 | } 84 | 85 | ActionType ToActionType(const int type) 86 | { 87 | return GetActionTypeEnums().at(static_cast(type)); 88 | } 89 | 90 | const QString &GetActionTypeName(const ActionType type) 91 | { 92 | return GetActionTypeName(static_cast(type)); 93 | } 94 | 95 | const QString &GetActionTypeName(const int type) 96 | { 97 | static const array names {{ 98 | "Task", 99 | "Channel", 100 | "Meeting", 101 | }}; 102 | 103 | return names.at(static_cast(type)); 104 | } 105 | 106 | const std::array& GetActionTypeEnums() 107 | { 108 | static const std::array enums {{ 109 | ActionType::TASK, 110 | ActionType::CHANNEL, 111 | ActionType::MEETING, 112 | }}; 113 | 114 | return enums; 115 | } 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/logging.cpp: -------------------------------------------------------------------------------- 1 | #include "logging.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace std; 11 | 12 | Logging *Logging::instance_ = {}; 13 | 14 | Logging::Logging() 15 | { 16 | assert(!instance_); 17 | instance_ = this; 18 | } 19 | 20 | Logging::~Logging() 21 | { 22 | assert(instance_); 23 | instance_ = {}; 24 | } 25 | 26 | void Logging::logMessageHandler(QtMsgType type, 27 | const QMessageLogContext &context, 28 | const QString &msg) 29 | { 30 | if (instance_) { 31 | instance_->onLogMessageHandler(type, context, msg); 32 | } 33 | } 34 | 35 | void Logging::onLogMessageHandler(QtMsgType type, 36 | const QMessageLogContext &context, 37 | const QString &msg) 38 | { 39 | array message_types = {{ 40 | "debug", "warn", "error", "fatal", "info", "system" 41 | }}; 42 | 43 | const auto now = QDateTime::currentDateTime().toString("yyyy-HH-mm HH:mm.zzz"); 44 | 45 | if (settings_.value("log-enabled", false).toBool()) { 46 | 47 | if (!logFile_) { 48 | open(); 49 | } 50 | 51 | QTextStream stream(logFile_.get()); 52 | 53 | stream 54 | << now 55 | << ' ' 56 | << message_types.at(type) 57 | << ' ' 58 | << (context.function ? context.function : "[no context]") 59 | << ' ' 60 | << msg 61 | << '\n'; 62 | 63 | if (type >= QtWarningMsg && context.function != nullptr) { 64 | // QMessageBox::warning(nullptr, message_types.at(type), 65 | // msg); 66 | emit message(message_types.at(type), msg); 67 | } 68 | } 69 | 70 | if (cout.good()) { 71 | 72 | cout << now.toStdString() 73 | << ' ' 74 | << message_types.at(type).toStdString() 75 | << ' ' 76 | << (context.function ? context.function : "[no context]") 77 | << ' ' 78 | << msg.toStdString() 79 | << endl; 80 | } 81 | } 82 | 83 | void Logging::changed() 84 | { 85 | if (settings_.value("log-enabled", false).toBool()) { 86 | open(); 87 | } else if (logFile_){ 88 | 89 | qDebug() << "Closing the log-file"; 90 | logFile_->reset(); 91 | } 92 | 93 | } 94 | 95 | void Logging::open() 96 | { 97 | const auto path = settings_.value("log-path", "f-crm.log").toString(); 98 | QIODevice::OpenMode options = QIODevice::WriteOnly; 99 | QString mode = "truncate"; 100 | if (settings_.value("log-append", false).toBool()) { 101 | options |= QIODevice::Append; 102 | mode = "append"; 103 | } else { 104 | options |= QIODevice::Truncate; 105 | } 106 | qDebug() << "Opening log-file: " << path << " [" << mode << ']'; 107 | logFile_ = make_unique(path); 108 | logFile_->open(options); 109 | } 110 | -------------------------------------------------------------------------------- /res/icons/addperson.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 40 | 44 | 45 | 48 | 52 | 53 | 56 | 60 | 61 | 62 | 82 | 85 | 90 | 97 | 104 | 105 | -------------------------------------------------------------------------------- /res/icons/updated_person.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 40 | 44 | 45 | 48 | 52 | 53 | 56 | 60 | 61 | 62 | 82 | 85 | 90 | 94 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /res/icons/edit_intent.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 40 | 44 | 45 | 48 | 52 | 53 | 54 | 74 | 82 | 88 | 94 | 100 | 104 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/actionproxymodel.cpp: -------------------------------------------------------------------------------- 1 | #include "src/actionproxymodel.h" 2 | #include "channel.h" 3 | #include "contact.h" 4 | #include 5 | 6 | 7 | ActionProxyModel::ActionProxyModel(ActionsModel *docModel, QObject *parent) 8 | : QSortFilterProxyModel(parent), model_{docModel} 9 | { 10 | setSourceModel(model_); 11 | } 12 | 13 | QVariant ActionProxyModel::data(const QModelIndex &ix, int role) const 14 | { 15 | //static const int h_name = model_->fieldIndex("name"); 16 | static const int h_state = model_->fieldIndex("state"); 17 | static const int h_type = model_->fieldIndex("type"); 18 | static const int h_channel_type = model_->fieldIndex("channel_type"); 19 | static const int h_person = model_->fieldIndex("person"); 20 | static const int h_start_date = model_->fieldIndex("start_date"); 21 | 22 | if (ix.isValid()) { 23 | if (role == Qt::DisplayRole || role == Qt::EditRole) { 24 | if (ix.column() == h_type) { 25 | return GetActionTypeName(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 26 | } 27 | 28 | if (ix.column() == h_state) { 29 | return GetActionStateName(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 30 | } 31 | 32 | if (ix.column() == h_person) { 33 | const auto id = model_->data(ix, Qt::DisplayRole).toInt(); 34 | if (id > 0) { 35 | QSqlQuery query(QStringLiteral("select name from contact where id = %1") 36 | .arg(id)); 37 | if (query.next()) { 38 | return query.value(0).toString(); 39 | } 40 | } 41 | } 42 | } else if (role == Qt::DecorationRole) { 43 | if (ix.column() == h_start_date) { 44 | const auto cix = index(ix.row(), h_type, {}); 45 | const int type = model_->data(cix, Qt::DisplayRole).toInt(); 46 | if (type == static_cast(ActionType::CHANNEL)) { 47 | const auto cix = index(ix.row(), h_channel_type, {}); 48 | const int ctype = model_->data(cix, Qt::DisplayRole).toInt(); 49 | return GetChannelStatusIcon(ctype); 50 | } 51 | 52 | return GetActionTypeIcon(std::max(0, type)); 53 | } 54 | 55 | if (ix.column() == h_state) { 56 | return GetActionStateIcon(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 57 | } 58 | 59 | if (ix.column() == h_type) { 60 | return GetActionTypeIcon(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 61 | } 62 | 63 | if (ix.column() == h_channel_type) { 64 | return GetChannelStatusIcon(std::max(0, model_->data(ix, Qt::DisplayRole).toInt())); 65 | } 66 | } 67 | } 68 | 69 | return model_->data(ix, role); 70 | } 71 | 72 | 73 | Qt::ItemFlags ActionProxyModel::flags(const QModelIndex &ix) const 74 | { 75 | static const int h_name = model_->fieldIndex("name"); 76 | 77 | if (ix.isValid()) { 78 | if ((ix.column() == h_name)) { 79 | return QSortFilterProxyModel::flags(ix); 80 | } 81 | } 82 | 83 | return QSortFilterProxyModel::flags(ix) & ~Qt::EditRole; 84 | } 85 | -------------------------------------------------------------------------------- /src/persondialog.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "contact.h" 4 | #include "src/persondialog.h" 5 | #include "ui_persondialog.h" 6 | 7 | PersonDialog::PersonDialog(ContactsModel& model, bool isPerson, int row, QWidget *parent) : 8 | QDialog(parent), 9 | ui(new Ui::PersonDialog), 10 | is_person_{isPerson}, 11 | row_{row}, 12 | model_{model} 13 | { 14 | ui->setupUi(this); 15 | 16 | this->setWindowTitle(QStringLiteral("Edit %1") 17 | .arg(isPerson ? "Person" : "Contact")); 18 | 19 | mapper_.setModel(&model_); 20 | 21 | for(auto e : GetContactStatusEnums()) { 22 | ui->status->addItem(GetContactStatusIcon(e), 23 | GetContactStatusName(e), 24 | static_cast(e)); 25 | } 26 | 27 | if (is_person_) { 28 | for(auto e : GetContactGenderEnums()) { 29 | ui->gender->addItem(GetContactGenderIcon(e), 30 | GetContactGenderName(e), 31 | static_cast(e)); 32 | } 33 | 34 | mapper_.addMapping(ui->gender, model_.fieldIndex("gender"), "currentData"); 35 | ui->status->setHidden(true); 36 | ui->statusLabel->setHidden(true); 37 | } else { 38 | mapper_.addMapping(ui->status, model_.fieldIndex("status"), "currentData"); 39 | ui->gender->setHidden(true); 40 | ui->genderLabel->setHidden(true); 41 | } 42 | 43 | for(int i = 0; i <= 5; i++) { 44 | ui->stars->addItem(QIcon(QStringLiteral(":/res/icons/%1star").arg(i)), "", i); 45 | } 46 | 47 | ui->stars->setIconSize({80, 16}); 48 | 49 | mapper_.addMapping(ui->stars, model_.fieldIndex("stars"), "currentData"); 50 | mapper_.addMapping(ui->favorite, model_.fieldIndex("favourite")); 51 | mapper_.addMapping(ui->name, model_.fieldIndex("name")); 52 | mapper_.addMapping(ui->address1, model_.fieldIndex("address1")); 53 | mapper_.addMapping(ui->address2, model_.fieldIndex("address2")); 54 | mapper_.addMapping(ui->city, model_.fieldIndex("city")); 55 | mapper_.addMapping(ui->postcode, model_.fieldIndex("postcode")); 56 | mapper_.addMapping(ui->region, model_.fieldIndex("region")); 57 | mapper_.addMapping(ui->state, model_.fieldIndex("state")); 58 | mapper_.addMapping(ui->country, model_.fieldIndex("country")); 59 | mapper_.addMapping(ui->notes, model_.fieldIndex("notes")); 60 | 61 | mapper_.setCurrentIndex(row); 62 | 63 | { 64 | const auto dix = model_.index(row, model_.fieldIndex("stars"), {}); 65 | int gender_val = model_.data(dix, Qt::EditRole).toInt(); 66 | ui->stars->setCurrentIndex(ui->stars->findData(gender_val)); 67 | } 68 | 69 | if (is_person_) { 70 | const auto dix = model_.index(row, model_.fieldIndex("gender"), {}); 71 | int gender_val = model_.data(dix, Qt::EditRole).toInt(); 72 | ui->gender->setCurrentIndex(ui->gender->findData(gender_val)); 73 | } else { 74 | const auto dix = model_.index(row, model_.fieldIndex("status"), {}); 75 | int status_val = model_.data(dix, Qt::EditRole).toInt(); 76 | ui->status->setCurrentIndex(ui->status->findData(status_val)); 77 | } 78 | } 79 | 80 | PersonDialog::~PersonDialog() 81 | { 82 | delete ui; 83 | } 84 | 85 | void PersonDialog::accept() 86 | { 87 | mapper_.submit(); 88 | 89 | QDialog::accept(); 90 | } 91 | 92 | 93 | -------------------------------------------------------------------------------- /res/icons/company.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 40 | 44 | 45 | 48 | 52 | 53 | 54 | 74 | 77 | 80 | 84 | 88 | 93 | 97 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # F-CRM 2 | 3 | **Customer Relations Management** for Freelancers and Individual Contractors. 4 | 5 | ![f-crm on Linux](doc/f-crm.png) 6 |
Screenshot from Debian Linux 7 | 8 | # Why? 9 | I am a freelancer. I need professional relations with my clients. This require a system where all contact-information, documents, emails, calls, - *everything* - is easily available when I need it. 10 | 11 | # Backstory 12 | I spent a full day trying to find a suitable solution. I looked for an open source, or at least affordable, desktop application. Nothing. Everything is *cloud* these days. It's just that - *I don't want cloud*. Cloud services are fragile. They may go tits up. They may get acquired and terminated. They may simply *terminate your service for whatever or no reason*. **There is no cloud!** It's just someone else's computer. I like dependable applications. They're faster to work with, have a richer set of UI controls, and they work whether Internet is available or not. 13 | 14 | I did try [odoo](https://www.odoo.com/) CRM and Sales modules, installing the backed on my own server. Odoo is very popular. It's an impressive project - but the CRM capabilities are too limited for my use. 15 | 16 | So, I decided to code a good Desktop CRM application from scratch in *one* week. One week later, Friday 23'rd 2018, I downloaded the binary .deb package from my build server, installed it on my PC, and started to use it. 17 | 18 | # Features 19 | The application is dead simple, with just the complexity and feature a high value Freelancer needs to do a brilliant job with sales and customer relations. 20 | 21 | - **Contact management** (companies or private persons). 22 | - **Contact-person management** at the companies. 23 | - **Intents** (pipelines) - some mini-projects with clear defined goals that you want to achieve. For example , sell something to, or get the attention from, a prospect. 24 | - **Actions** - Steps / tasks to perform to move an intent forward against completion. For example - send a follow-up mail at a specific date. 25 | - **Document management** documents and mails are linked to customers, persons, intents or actions. 26 | - **Journal** - a list of all the relevant things that has happened within the relation with a contact. This is updated automatically when you add or change information. 27 | - **Data is stored locally** in a sqlite database. 28 | - **Integration with email clients** so that we can send and look at sent/received emails directly from *f-crm*. Currently Thunderbird is tested. 29 | 30 | # Supported platforms 31 | - Linux AppImage (built from Ubuntu Trusty LTS) 32 | - Debian Stretch 33 | - Debian Testing 34 | - Ubuntu Xenial (LTS) 35 | - Ubuntu Bionic (LTS) 36 | - macOS 37 | - Windows (Windows Vista and up, 64 bit builds) 38 | 39 | # Prebuilt binaries 40 | When I [release](https://github.com/jgaa/f-crm/releases) new versions, I provide binaries for the following platforms: 41 | - Linux (AppImage) 42 | - macOS (.dmg file) 43 | - Windows (.msi file for Windows Installer) 44 | 45 | # How to build 46 | I use QT Creator for this project. There are [scripts](scripts) for building and packaging it from the command-line. 47 | 48 | There is also a [Jenkinsfile](ci/jenkins/Jenkinsfile.groovy) and [docker-files](ci/jenkins/) to build it on all platforms from Jenkins. 49 | 50 | # Current status 51 | **Under active development**. I am using it myself, and will fix bugs and add nice features as other users suggest them or I discover them myself. 52 | 53 | -------------------------------------------------------------------------------- /res/icons/delete_action.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 40 | 44 | 45 | 48 | 52 | 53 | 54 | 74 | 79 | 83 | 87 | 92 | 99 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /res/icons/edit_document.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 40 | 44 | 45 | 48 | 52 | 53 | 54 | 74 | 77 | 81 | 84 | 90 | 96 | 99 | 100 | 104 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /res/icons/skype.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 62 | 68 | 74 | 75 | 76 | 77 | --------------------------------------------------------------------------------