Initial commit: Kareer, a Kirigami job application tracker

C++/Qt6/KDE Frameworks 6 app with a SQLite-backed data layer, a
Sankey-diagram dashboard, and a full CLI (add/list/show/update/stage/
delete/stats/stages) for scripting from other tools.
This commit is contained in:
2026-07-03 10:35:16 -05:00
commit 337eb1a6bd
33 changed files with 4071 additions and 0 deletions
+206
View File
@@ -0,0 +1,206 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "jobsmodel.h"
#include "jobstage.h"
using namespace Qt::Literals::StringLiterals;
JobsModel::JobsModel(QObject *parent)
: QAbstractListModel(parent)
{
refresh();
}
void JobsModel::refresh()
{
beginResetModel();
m_jobs = m_db.allJobs();
endResetModel();
Q_EMIT countChanged();
}
int JobsModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_jobs.size();
}
QVariant JobsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_jobs.size()) {
return {};
}
const Job &job = m_jobs.at(index.row());
switch (role) {
case IdRole:
return job.id;
case CompanyRole:
return job.company;
case TitleRole:
return job.title;
case LocationRole:
return job.location;
case RemoteTypeRole:
return job.remoteType;
case SourceRole:
return job.source;
case UrlRole:
return job.url;
case DateAppliedRole:
return job.dateApplied;
case SalaryMinRole:
return job.salaryMin;
case SalaryMaxRole:
return job.salaryMax;
case SalaryExpectationRole:
return job.salaryExpectation;
case CurrencyRole:
return job.currency;
case NotesRole:
return job.notes;
case ContactRole:
return job.contact;
case StageRole:
return job.stage;
case StageColorRole:
return JobStage::color(job.stage);
case CreatedAtRole:
return job.createdAt;
case UpdatedAtRole:
return job.updatedAt;
default:
return {};
}
}
QHash<int, QByteArray> JobsModel::roleNames() const
{
return {
{IdRole, "jobId"},
{CompanyRole, "company"},
{TitleRole, "title"},
{LocationRole, "location"},
{RemoteTypeRole, "remoteType"},
{SourceRole, "source"},
{UrlRole, "url"},
{DateAppliedRole, "dateApplied"},
{SalaryMinRole, "salaryMin"},
{SalaryMaxRole, "salaryMax"},
{SalaryExpectationRole, "salaryExpectation"},
{CurrencyRole, "currency"},
{NotesRole, "notes"},
{ContactRole, "contact"},
{StageRole, "stage"},
{StageColorRole, "stageColor"},
{CreatedAtRole, "createdAt"},
{UpdatedAtRole, "updatedAt"},
};
}
QStringList JobsModel::stages() const
{
return JobStage::canonicalStages();
}
Job JobsModel::jobFromMap(const QVariantMap &fields)
{
Job job;
job.company = fields.value(u"company"_s).toString();
job.title = fields.value(u"title"_s).toString();
job.location = fields.value(u"location"_s).toString();
job.remoteType = fields.value(u"remoteType"_s).toString();
job.source = fields.value(u"source"_s).toString();
job.url = fields.value(u"url"_s).toString();
const QVariant dateValue = fields.value(u"dateApplied"_s);
job.dateApplied = dateValue.canConvert<QDate>() ? dateValue.toDate() : QDate::fromString(dateValue.toString(), Qt::ISODate);
job.salaryMin = fields.value(u"salaryMin"_s, -1).toInt();
job.salaryMax = fields.value(u"salaryMax"_s, -1).toInt();
job.salaryExpectation = fields.value(u"salaryExpectation"_s, -1).toInt();
job.currency = fields.value(u"currency"_s, u"USD"_s).toString();
if (job.currency.isEmpty()) {
job.currency = u"USD"_s;
}
job.notes = fields.value(u"notes"_s).toString();
job.contact = fields.value(u"contact"_s).toString();
job.stage = fields.value(u"stage"_s).toString();
return job;
}
QVariantMap JobsModel::mapFromJob(const Job &job)
{
return {
{u"id"_s, job.id},
{u"company"_s, job.company},
{u"title"_s, job.title},
{u"location"_s, job.location},
{u"remoteType"_s, job.remoteType},
{u"source"_s, job.source},
{u"url"_s, job.url},
{u"dateApplied"_s, job.dateApplied},
{u"salaryMin"_s, job.salaryMin},
{u"salaryMax"_s, job.salaryMax},
{u"salaryExpectation"_s, job.salaryExpectation},
{u"currency"_s, job.currency},
{u"notes"_s, job.notes},
{u"contact"_s, job.contact},
{u"stage"_s, job.stage},
{u"createdAt"_s, job.createdAt},
{u"updatedAt"_s, job.updatedAt},
};
}
QVariantMap JobsModel::jobData(int id) const
{
const auto job = m_db.jobById(id);
if (!job) {
return {};
}
return mapFromJob(*job);
}
bool JobsModel::addJob(const QVariantMap &fields)
{
Job job = jobFromMap(fields);
const bool ok = m_db.addJob(job);
if (ok) {
refresh();
}
return ok;
}
bool JobsModel::updateJob(int id, const QVariantMap &fields)
{
Job job = jobFromMap(fields);
job.id = id;
const bool ok = m_db.updateJob(job);
if (ok) {
refresh();
}
return ok;
}
bool JobsModel::setStage(int id, const QString &stage)
{
const bool ok = m_db.setStage(id, stage);
if (ok) {
refresh();
}
return ok;
}
bool JobsModel::removeJob(int id)
{
const bool ok = m_db.deleteJob(id);
if (ok) {
refresh();
}
return ok;
}
QString JobsModel::lastError() const
{
return m_db.lastError();
}